317 lines
10 KiB
Go
317 lines
10 KiB
Go
package netstack
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestApplyRunsCleanupThenStartsRulesAndDNS(t *testing.T) {
|
|
var calls []string
|
|
manager := New("/data/adb/modules/rknnovpn", map[string]string{"A": "B"}, func(scriptPath string, command string, env map[string]string) error {
|
|
calls = append(calls, filepath.Base(scriptPath)+":"+command+":"+env["A"])
|
|
return nil
|
|
})
|
|
|
|
report := manager.Apply()
|
|
if err := report.Err(); err != nil {
|
|
t.Fatalf("apply should succeed: %v %#v", err, report)
|
|
}
|
|
want := []string{
|
|
"dns.sh:stop:B",
|
|
"iptables.sh:stop:B",
|
|
"iptables.sh:start:B",
|
|
"dns.sh:start:B",
|
|
"privacy_guard.sh:start:B",
|
|
}
|
|
if strings.Join(calls, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("unexpected calls: got %#v want %#v", calls, want)
|
|
}
|
|
}
|
|
|
|
func TestApplyPrivacyGuardFailureWarnsWithoutRollback(t *testing.T) {
|
|
var calls []string
|
|
manager := New("/data/adb/modules/rknnovpn", nil, func(scriptPath string, command string, env map[string]string) error {
|
|
call := filepath.Base(scriptPath) + ":" + command
|
|
calls = append(calls, call)
|
|
if call == "privacy_guard.sh:start" {
|
|
return errors.New("privacy guard unsupported")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
report := manager.Apply()
|
|
if err := report.Err(); err != nil {
|
|
t.Fatalf("privacy guard warning must not fail apply: %v %#v", err, report)
|
|
}
|
|
if report.RollbackApplied {
|
|
t.Fatalf("privacy guard warning must not roll back netstack: %#v", report)
|
|
}
|
|
if len(report.Warnings) != 1 || !strings.Contains(report.Warnings[0], "privacy guard unsupported") {
|
|
t.Fatalf("expected privacy guard warning, got %#v", report)
|
|
}
|
|
if got := strings.Join(calls, ","); strings.Contains(got, "iptables.sh:stop,iptables.sh:stop") {
|
|
t.Fatalf("unexpected rollback cleanup: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyDNSFailureRollsBackAndReturnsDNSCode(t *testing.T) {
|
|
var calls []string
|
|
manager := New("/data/adb/modules/rknnovpn", nil, func(scriptPath string, command string, env map[string]string) error {
|
|
call := filepath.Base(scriptPath) + ":" + command
|
|
calls = append(calls, call)
|
|
if call == "dns.sh:start" {
|
|
return errors.New("dns failed")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
report := manager.Apply()
|
|
err := report.Err()
|
|
if err == nil {
|
|
t.Fatalf("expected apply error: %#v", report)
|
|
}
|
|
netErr, ok := err.(*Error)
|
|
if !ok {
|
|
t.Fatalf("expected netstack Error, got %T", err)
|
|
}
|
|
if netErr.Code != "DNS_APPLY_FAILED" {
|
|
t.Fatalf("expected DNS_APPLY_FAILED, got %#v", netErr)
|
|
}
|
|
if !report.RollbackApplied {
|
|
t.Fatalf("expected rollback flag: %#v", report)
|
|
}
|
|
if got := strings.Join(calls, ","); !strings.Contains(got, "iptables.sh:stop") {
|
|
t.Fatalf("expected rollback cleanup, got calls %s", got)
|
|
}
|
|
}
|
|
|
|
func TestCleanupTreatsMissingScriptsAsAlreadyClean(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", nil, func(scriptPath string, command string, env map[string]string) error {
|
|
return errors.New("script not found: " + scriptPath + ": no such file or directory")
|
|
})
|
|
|
|
report := manager.Cleanup()
|
|
if err := report.Err(); err != nil {
|
|
t.Fatalf("missing cleanup scripts should be no-op: %v %#v", err, report)
|
|
}
|
|
for _, step := range report.Steps {
|
|
if step.Status != "already_clean" {
|
|
t.Fatalf("expected already_clean step, got %#v", report)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVerifyCleanupReportsRKNnoVPNRulesAndRoutes(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", map[string]string{
|
|
"FWMARK": "0x2023",
|
|
"ROUTE_TABLE": "2023",
|
|
"ROUTE_TABLE_V6": "2024",
|
|
}, nil).WithExecCommand(func(name string, args ...string) (string, error) {
|
|
key := name + " " + strings.Join(args, " ")
|
|
switch key {
|
|
case "iptables -w 10 -t mangle -S":
|
|
return "-N RKNNOVPN_OUT\n-A OUTPUT -j RKNNOVPN_OUT", nil
|
|
case "ip rule show":
|
|
return "100: from all fwmark 0x2023 lookup 2023", nil
|
|
case "ip route show table 2023":
|
|
return "local default dev lo scope host", nil
|
|
case "pidof sing-box":
|
|
return "", nil
|
|
default:
|
|
return "", nil
|
|
}
|
|
})
|
|
|
|
report := manager.VerifyCleanup()
|
|
if report.Status != "failed" {
|
|
t.Fatalf("expected failed verify report, got %#v", report)
|
|
}
|
|
if err := report.Err(); err == nil {
|
|
t.Fatalf("verify-cleanup leftovers must produce an error: %#v", report)
|
|
}
|
|
text := strings.Join(report.Leftovers, "\n")
|
|
for _, want := range []string{
|
|
"iptables mangle rule remains",
|
|
"ip rule remains",
|
|
"ip route table 2023 still has routes",
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("expected leftover %q in %s", want, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRuleLineMatchesExactFwmarkAndTable(t *testing.T) {
|
|
if !RuleLineMatches("100: from all fwmark 0x2023 lookup 2023", "0x2023", "2023") {
|
|
t.Fatal("expected exact RKNnoVPN fwmark/table to match")
|
|
}
|
|
if RuleLineMatches("100: from all fwmark 0x20230 lookup 20230", "0x2023", "2023") {
|
|
t.Fatal("substring fwmark/table must not match")
|
|
}
|
|
if RuleLineMatches("100: from all fwmark 0x2023 lookup 9999", "0x2023", "2023") {
|
|
t.Fatal("matching fwmark with wrong table must not match")
|
|
}
|
|
if RuleLineMatches("100: from all fwmark 0x9999 lookup 2023", "0x2023", "2023") {
|
|
t.Fatal("matching table with wrong fwmark must not match")
|
|
}
|
|
if !RuleLineMatches("100: from all fwmark 0x2023/0xffffffff lookup 2023", "0x2023", "2023") {
|
|
t.Fatal("expected masked fwmark to match")
|
|
}
|
|
}
|
|
|
|
func TestVerifyCleanupReportsUDPAndWildcardLocalListeners(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", map[string]string{
|
|
"TPROXY_PORT": "10853",
|
|
"DNS_PORT": "10856",
|
|
}, nil).WithExecCommand(func(name string, args ...string) (string, error) {
|
|
key := name + " " + strings.Join(args, " ")
|
|
switch key {
|
|
case "ss -H -lntup", "ss -lntup":
|
|
return "", errors.New("ss unavailable")
|
|
case "cat /proc/net/udp":
|
|
return " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n 1: 00000000:2A68 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1 2 0000000000000000 0\n", nil
|
|
default:
|
|
return "", nil
|
|
}
|
|
})
|
|
|
|
report := manager.VerifyCleanup()
|
|
if err := report.Err(); err == nil {
|
|
t.Fatalf("UDP wildcard listener must fail cleanup verification: %#v", report)
|
|
}
|
|
text := strings.Join(report.Leftovers, "\n")
|
|
if !strings.Contains(text, "udp port 10856 still listening on 0.0.0.0") {
|
|
t.Fatalf("expected UDP wildcard leftover, got %s", text)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCleanupWarnsForForeignSingBox(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", nil, nil).WithExecCommand(func(name string, args ...string) (string, error) {
|
|
key := name + " " + strings.Join(args, " ")
|
|
switch key {
|
|
case "pidof sing-box":
|
|
return "1234", nil
|
|
case "readlink /proc/1234/exe":
|
|
return "/data/adb/modules/other/bin/sing-box\n", nil
|
|
case "cat /proc/1234/cmdline":
|
|
return "/data/adb/modules/other/bin/sing-box\x00run\x00-c\x00/data/adb/modules/other/config.json", nil
|
|
default:
|
|
return "", nil
|
|
}
|
|
})
|
|
|
|
report := manager.VerifyCleanup()
|
|
if err := report.Err(); err != nil {
|
|
t.Fatalf("foreign sing-box must not be a hard cleanup failure: %v %#v", err, report)
|
|
}
|
|
if len(report.Warnings) != 1 || !strings.Contains(report.Warnings[0], "other sing-box process") {
|
|
t.Fatalf("expected foreign sing-box warning, got %#v", report)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCleanupIgnoresMissingOptionalCommandAndTables(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", map[string]string{
|
|
"FWMARK": "0x2023",
|
|
"ROUTE_TABLE": "2023",
|
|
}, nil).WithExecCommand(func(name string, args ...string) (string, error) {
|
|
if strings.HasPrefix(name, "iptables") || strings.HasPrefix(name, "ip6tables") {
|
|
return "table does not exist", errors.New("exit status 1")
|
|
}
|
|
if name == "ip" {
|
|
return "", nil
|
|
}
|
|
return "", nil
|
|
})
|
|
|
|
report := manager.VerifyCleanup()
|
|
if len(report.Leftovers) != 0 {
|
|
t.Fatalf("missing optional tables should not be leftovers: %#v", report)
|
|
}
|
|
if report.Status != "ok" {
|
|
t.Fatalf("expected ok verify report, got %#v", report)
|
|
}
|
|
}
|
|
|
|
func TestVerifyReturnsVerifyCode(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", nil, func(scriptPath string, command string, env map[string]string) error {
|
|
if filepath.Base(scriptPath) == "dns.sh" && command == "status" {
|
|
return errors.New("dns hook missing")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
report := manager.Verify()
|
|
err := report.Err()
|
|
if err == nil {
|
|
t.Fatalf("expected verify error: %#v", report)
|
|
}
|
|
netErr, ok := err.(*Error)
|
|
if !ok {
|
|
t.Fatalf("expected netstack Error, got %T", err)
|
|
}
|
|
if netErr.Code != "NETSTACK_VERIFY_FAILED" {
|
|
t.Fatalf("expected NETSTACK_VERIFY_FAILED, got %#v", netErr)
|
|
}
|
|
}
|
|
|
|
func TestVerifyRunsRuleAndDNSStatus(t *testing.T) {
|
|
var calls []string
|
|
manager := New("/data/adb/modules/rknnovpn", map[string]string{"A": "B"}, func(scriptPath string, command string, env map[string]string) error {
|
|
calls = append(calls, filepath.Base(scriptPath)+":"+command+":"+env["A"])
|
|
return nil
|
|
})
|
|
|
|
report := manager.Verify()
|
|
if err := report.Err(); err != nil {
|
|
t.Fatalf("verify should succeed: %v %#v", err, report)
|
|
}
|
|
want := []string{
|
|
"iptables.sh:status:B",
|
|
"dns.sh:status:B",
|
|
}
|
|
if strings.Join(calls, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("unexpected calls: got %#v want %#v", calls, want)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveLocalPortsSkipsDisabledHelpersAndAPI(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", map[string]string{
|
|
"TPROXY_PORT": "10853",
|
|
"DNS_PORT": "10856",
|
|
"API_PORT": "0",
|
|
"SOCKS_PORT": "0",
|
|
"HTTP_PORT": "0",
|
|
}, nil)
|
|
|
|
got := manager.effectiveLocalPorts()
|
|
if strings.Join(intsToStrings(got), ",") != "10853,10856" {
|
|
t.Fatalf("expected only tproxy/dns ports by default, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveLocalPortsIncludesChainedProxyPorts(t *testing.T) {
|
|
manager := New("/data/adb/modules/rknnovpn", map[string]string{
|
|
"TPROXY_PORT": "10853",
|
|
"DNS_PORT": "10856",
|
|
"API_PORT": "0",
|
|
"SOCKS_PORT": "0",
|
|
"HTTP_PORT": "0",
|
|
"CHAIN_PROXY_PORTS": "10808 10809 bad 10808",
|
|
}, nil)
|
|
|
|
got := manager.effectiveLocalPorts()
|
|
if strings.Join(intsToStrings(got), ",") != "10853,10856,10808,10809" {
|
|
t.Fatalf("expected chained proxy ports, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func intsToStrings(values []int) []string {
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
result = append(result, strconv.Itoa(value))
|
|
}
|
|
return result
|
|
}
|