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
743 lines
19 KiB
Go
743 lines
19 KiB
Go
package netstack
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.zapret.moe/zapretdiscordyoutube/RKNnoVPN/runtime/internal/modulecontract"
|
|
)
|
|
|
|
type ExecScriptFunc func(scriptPath string, command string, env map[string]string) error
|
|
type ExecCommandFunc func(name string, args ...string) (string, error)
|
|
|
|
type Step struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
type Report struct {
|
|
Operation string `json:"operation"`
|
|
Status string `json:"status"`
|
|
Steps []Step `json:"steps"`
|
|
Errors []string `json:"errors,omitempty"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Leftovers []string `json:"leftovers,omitempty"`
|
|
RollbackApplied bool `json:"rollbackApplied,omitempty"`
|
|
}
|
|
|
|
type Error struct {
|
|
Operation string
|
|
Code string
|
|
Report Report
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
if len(e.Report.Errors) == 0 {
|
|
return e.Operation + " failed"
|
|
}
|
|
return fmt.Sprintf("%s failed: %s", e.Operation, strings.Join(e.Report.Errors, "; "))
|
|
}
|
|
|
|
type Manager struct {
|
|
dataDir string
|
|
env map[string]string
|
|
execScript ExecScriptFunc
|
|
execCommand ExecCommandFunc
|
|
}
|
|
|
|
func New(dataDir string, env map[string]string, execScript ExecScriptFunc) Manager {
|
|
return Manager{
|
|
dataDir: dataDir,
|
|
env: env,
|
|
execScript: execScript,
|
|
}
|
|
}
|
|
|
|
func (m Manager) WithExecCommand(execCommand ExecCommandFunc) Manager {
|
|
m.execCommand = execCommand
|
|
return m
|
|
}
|
|
|
|
func (m Manager) Apply() Report {
|
|
report := Report{Operation: "apply", Status: "ok"}
|
|
m.cleanupInto(&report, true)
|
|
if report.Status == "failed" {
|
|
return report
|
|
}
|
|
|
|
if err := m.run("iptables", "start"); err != nil {
|
|
report.addFailure("iptables-start", err)
|
|
report.RollbackApplied = true
|
|
m.cleanupInto(&report, true)
|
|
return report
|
|
}
|
|
report.addOK("iptables-start", "")
|
|
|
|
if err := m.run("dns", "start"); err != nil {
|
|
report.addFailure("dns-start", err)
|
|
report.RollbackApplied = true
|
|
m.cleanupInto(&report, true)
|
|
return report
|
|
}
|
|
report.addOK("dns-start", "")
|
|
|
|
if err := m.run("privacy_guard", "start"); err != nil {
|
|
report.addWarning("privacy-guard-start", err)
|
|
} else {
|
|
report.addOK("privacy-guard-start", "")
|
|
}
|
|
return report
|
|
}
|
|
|
|
func (m Manager) Cleanup() Report {
|
|
report := Report{Operation: "cleanup", Status: "ok"}
|
|
m.cleanupInto(&report, true)
|
|
return report
|
|
}
|
|
|
|
func (m Manager) Verify() Report {
|
|
report := Report{Operation: "verify", Status: "ok"}
|
|
|
|
if err := m.run("iptables", "status"); err != nil {
|
|
report.addFailure("iptables-status", err)
|
|
return report
|
|
}
|
|
report.addOK("iptables-status", "")
|
|
|
|
if err := m.run("dns", "status"); err != nil {
|
|
report.addFailure("dns-status", err)
|
|
return report
|
|
}
|
|
report.addOK("dns-status", "")
|
|
return report
|
|
}
|
|
|
|
func (m Manager) VerifyCleanup() Report {
|
|
report := Report{Operation: "verify-cleanup", Status: "ok"}
|
|
findings := m.collectCleanupFindings()
|
|
leftovers := findings.leftovers
|
|
report.Leftovers = leftovers
|
|
report.Warnings = findings.warnings
|
|
if len(leftovers) == 0 {
|
|
report.addOK("verify-cleanup", "")
|
|
return report
|
|
}
|
|
detail := strings.Join(leftovers, "; ")
|
|
report.Status = "failed"
|
|
report.Steps = append(report.Steps, Step{
|
|
Name: "verify-cleanup",
|
|
Status: "failed",
|
|
Detail: detail,
|
|
})
|
|
report.Errors = append(report.Errors, detail)
|
|
return report
|
|
}
|
|
|
|
func (m Manager) CollectLeftovers() []string {
|
|
return m.collectCleanupFindings().leftovers
|
|
}
|
|
|
|
type cleanupFindings struct {
|
|
leftovers []string
|
|
warnings []string
|
|
}
|
|
|
|
func (m Manager) collectCleanupFindings() cleanupFindings {
|
|
if m.execCommand == nil {
|
|
return cleanupFindings{leftovers: []string{"exec command function is nil"}}
|
|
}
|
|
|
|
findings := cleanupFindings{
|
|
leftovers: make([]string, 0),
|
|
warnings: make([]string, 0),
|
|
}
|
|
add := func(format string, args ...interface{}) {
|
|
findings.leftovers = append(findings.leftovers, fmt.Sprintf(format, args...))
|
|
}
|
|
warn := func(format string, args ...interface{}) {
|
|
findings.warnings = append(findings.warnings, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
for _, spec := range []struct {
|
|
bin string
|
|
table string
|
|
}{
|
|
{bin: "iptables", table: "raw"},
|
|
{bin: "iptables", table: "mangle"},
|
|
{bin: "iptables", table: "nat"},
|
|
{bin: "iptables", table: "filter"},
|
|
{bin: "ip6tables", table: "raw"},
|
|
{bin: "ip6tables", table: "mangle"},
|
|
{bin: "ip6tables", table: "nat"},
|
|
{bin: "ip6tables", table: "filter"},
|
|
{bin: "iptables-nft", table: "raw"},
|
|
{bin: "iptables-nft", table: "mangle"},
|
|
{bin: "iptables-nft", table: "nat"},
|
|
{bin: "iptables-nft", table: "filter"},
|
|
{bin: "ip6tables-nft", table: "raw"},
|
|
{bin: "ip6tables-nft", table: "mangle"},
|
|
{bin: "ip6tables-nft", table: "nat"},
|
|
{bin: "ip6tables-nft", table: "filter"},
|
|
} {
|
|
out, err := m.execCommand(spec.bin, "-w", "10", "-t", spec.table, "-S")
|
|
if err != nil {
|
|
if isMissingCommandError(err) || isMissingKernelTableOutput(out) {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(out) != "" {
|
|
add("%s %s check failed: %v: %s", spec.bin, spec.table, err, firstLine(out))
|
|
} else {
|
|
add("%s %s check failed: %v", spec.bin, spec.table, err)
|
|
}
|
|
continue
|
|
}
|
|
if line := firstLineContaining(out, "RKNNOVPN"); line != "" {
|
|
add("%s %s rule remains: %s", spec.bin, spec.table, line)
|
|
}
|
|
}
|
|
|
|
mark := strings.ToLower(m.envValue("FWMARK"))
|
|
routeTable := m.envValue("ROUTE_TABLE")
|
|
routeTableV6 := m.envValue("ROUTE_TABLE_V6")
|
|
for _, spec := range []struct {
|
|
name string
|
|
args []string
|
|
table string
|
|
}{
|
|
{name: "ip rule", args: []string{"rule", "show"}, table: routeTable},
|
|
{name: "ip -6 rule", args: []string{"-6", "rule", "show"}, table: routeTableV6},
|
|
} {
|
|
out, err := m.execCommand("ip", spec.args...)
|
|
if err != nil {
|
|
add("%s check failed: %v", spec.name, err)
|
|
continue
|
|
}
|
|
for _, line := range splitLines(out) {
|
|
if RuleLineMatches(line, mark, spec.table) {
|
|
add("%s remains: %s", spec.name, strings.TrimSpace(line))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, spec := range []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{name: "ip route table " + routeTable, args: []string{"route", "show", "table", routeTable}},
|
|
{name: "ip -6 route table " + routeTableV6, args: []string{"-6", "route", "show", "table", routeTableV6}},
|
|
} {
|
|
out, err := m.execCommand("ip", spec.args...)
|
|
if err != nil {
|
|
if isMissingRouteTableOutput(out) {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
add("%s check failed: %v", spec.name, err)
|
|
} else {
|
|
add("%s check failed: %v: %s", spec.name, err, firstLine(out))
|
|
}
|
|
continue
|
|
}
|
|
if strings.TrimSpace(out) != "" {
|
|
add("%s still has routes: %s", spec.name, firstLine(out))
|
|
}
|
|
}
|
|
|
|
if out, _ := m.execCommand("pidof", "sing-box"); strings.TrimSpace(out) != "" {
|
|
for _, rawPID := range strings.Fields(out) {
|
|
pid, err := strconv.Atoi(rawPID)
|
|
if err != nil || pid <= 0 {
|
|
continue
|
|
}
|
|
owned, ownerDetail := m.processIsOwnedSingBox(pid)
|
|
if owned {
|
|
add("RKNnoVPN sing-box process still running: pid=%d %s", pid, ownerDetail)
|
|
} else {
|
|
warn("other sing-box process detected during cleanup verification: pid=%d %s", pid, ownerDetail)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, port := range m.effectiveLocalPorts() {
|
|
for _, listener := range m.findLocalListeners(port) {
|
|
if listener.PID > 0 {
|
|
owned, ownerDetail := m.processIsOwnedSingBox(listener.PID)
|
|
if owned {
|
|
add("RKNnoVPN %s port %d still listening on %s: pid=%d %s", listener.Proto, port, listener.Address, listener.PID, ownerDetail)
|
|
} else {
|
|
warn("other process owns %s port %d on %s: pid=%d %s", listener.Proto, port, listener.Address, listener.PID, ownerDetail)
|
|
}
|
|
continue
|
|
}
|
|
add("%s port %d still listening on %s (owner unknown)", listener.Proto, port, listener.Address)
|
|
}
|
|
}
|
|
|
|
for _, path := range modulecontract.NewPaths(m.dataDir).RuntimeSnapshotFiles() {
|
|
if _, err := os.Stat(path); err == nil {
|
|
add("stale runtime file remains: %s", path)
|
|
}
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
func (r Report) Err() error {
|
|
if len(r.Errors) == 0 {
|
|
return nil
|
|
}
|
|
code := "NETSTACK_FAILED"
|
|
for _, step := range r.Steps {
|
|
if step.Status != "failed" {
|
|
continue
|
|
}
|
|
switch step.Name {
|
|
case "iptables-start":
|
|
code = "RULES_NOT_APPLIED"
|
|
case "dns-start":
|
|
code = "DNS_APPLY_FAILED"
|
|
case "dns-stop", "iptables-stop":
|
|
code = "NETSTACK_CLEANUP_FAILED"
|
|
case "dns-status", "iptables-status":
|
|
code = "NETSTACK_VERIFY_FAILED"
|
|
}
|
|
break
|
|
}
|
|
return &Error{Operation: r.Operation, Code: code, Report: r}
|
|
}
|
|
|
|
func (r *Report) addOK(name string, detail string) {
|
|
r.Steps = append(r.Steps, Step{Name: name, Status: "ok", Detail: detail})
|
|
}
|
|
|
|
func (r *Report) addAlreadyClean(name string, detail string) {
|
|
r.Steps = append(r.Steps, Step{Name: name, Status: "already_clean", Detail: detail})
|
|
}
|
|
|
|
func (r *Report) addFailure(name string, err error) {
|
|
r.Status = "failed"
|
|
detail := ""
|
|
if err != nil {
|
|
detail = err.Error()
|
|
}
|
|
r.Steps = append(r.Steps, Step{Name: name, Status: "failed", Detail: detail})
|
|
r.Errors = append(r.Errors, name+": "+detail)
|
|
}
|
|
|
|
func (r *Report) addWarning(name string, err error) {
|
|
detail := ""
|
|
if err != nil {
|
|
detail = err.Error()
|
|
}
|
|
r.Steps = append(r.Steps, Step{Name: name, Status: "warning", Detail: detail})
|
|
r.Warnings = append(r.Warnings, name+": "+detail)
|
|
}
|
|
|
|
func (m Manager) cleanupInto(report *Report, tolerateMissing bool) {
|
|
for _, name := range []string{"dns", "iptables"} {
|
|
stepName := name + "-stop"
|
|
err := m.run(name, "stop")
|
|
switch {
|
|
case err == nil:
|
|
report.addOK(stepName, "")
|
|
case tolerateMissing && isMissingScriptError(err):
|
|
report.addAlreadyClean(stepName, err.Error())
|
|
default:
|
|
report.addFailure(stepName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m Manager) run(name string, command string) error {
|
|
if m.execScript == nil {
|
|
return fmt.Errorf("exec script function is nil")
|
|
}
|
|
paths := modulecontract.NewPaths(m.dataDir)
|
|
switch name {
|
|
case "dns":
|
|
return m.execScript(paths.DNSScript(), command, m.env)
|
|
case "iptables":
|
|
return m.execScript(paths.IPTablesScript(), command, m.env)
|
|
case "privacy_guard":
|
|
return m.execScript(paths.PrivacyGuardScript(), command, m.env)
|
|
default:
|
|
return fmt.Errorf("unknown netstack script %q", name)
|
|
}
|
|
}
|
|
|
|
func isMissingScriptError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
lower := strings.ToLower(err.Error())
|
|
return strings.Contains(lower, "script not found:") ||
|
|
strings.Contains(lower, "no such file or directory")
|
|
}
|
|
|
|
func isMissingKernelTableOutput(output string) bool {
|
|
lower := strings.ToLower(output)
|
|
return strings.Contains(lower, "table does not exist") ||
|
|
strings.Contains(lower, "can't initialize") ||
|
|
strings.Contains(lower, "does not exist")
|
|
}
|
|
|
|
func isMissingCommandError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
lower := strings.ToLower(err.Error())
|
|
return strings.Contains(lower, "executable file not found") ||
|
|
strings.Contains(lower, "no such file or directory")
|
|
}
|
|
|
|
func isMissingRouteTableOutput(output string) bool {
|
|
lower := strings.ToLower(output)
|
|
return strings.Contains(lower, "fib table does not exist") ||
|
|
strings.Contains(lower, "no such process") ||
|
|
strings.Contains(lower, "no such file")
|
|
}
|
|
|
|
// RuleLineMatches reports whether an `ip rule show` line belongs to the
|
|
// RKNnoVPN fwmark/table contract. It intentionally avoids substring matches
|
|
// so unrelated marks such as 0x20230 do not look like RKNnoVPN leftovers.
|
|
func RuleLineMatches(line string, mark string, table string) bool {
|
|
fields := strings.Fields(strings.ToLower(line))
|
|
wantMark := strings.ToLower(strings.TrimSpace(mark))
|
|
wantTable := strings.TrimSpace(table)
|
|
markOK := false
|
|
tableOK := false
|
|
for i, field := range fields {
|
|
if field == "fwmark" && wantMark != "" && i+1 < len(fields) {
|
|
got := fields[i+1]
|
|
if got == wantMark || strings.HasPrefix(got, wantMark+"/") {
|
|
markOK = true
|
|
}
|
|
}
|
|
if (field == "lookup" || field == "table") && wantTable != "" && i+1 < len(fields) {
|
|
if fields[i+1] == wantTable {
|
|
tableOK = true
|
|
}
|
|
}
|
|
}
|
|
return markOK && tableOK
|
|
}
|
|
|
|
func (m Manager) effectiveLocalPorts() []int {
|
|
ports := []int{
|
|
valueOrDefaultInt(m.envInt("TPROXY_PORT"), 10853),
|
|
valueOrDefaultInt(m.envInt("DNS_PORT"), 10856),
|
|
m.envInt("API_PORT"),
|
|
m.envInt("SOCKS_PORT"),
|
|
m.envInt("HTTP_PORT"),
|
|
}
|
|
for _, field := range strings.Fields(m.envValue("CHAIN_PROXY_PORTS")) {
|
|
port, err := strconv.Atoi(field)
|
|
if err == nil {
|
|
ports = append(ports, port)
|
|
}
|
|
}
|
|
seen := make(map[int]bool, len(ports))
|
|
result := make([]int, 0, len(ports))
|
|
for _, port := range ports {
|
|
if port <= 0 || seen[port] {
|
|
continue
|
|
}
|
|
seen[port] = true
|
|
result = append(result, port)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m Manager) envValue(key string) string {
|
|
if m.env == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(m.env[key])
|
|
}
|
|
|
|
func (m Manager) envInt(key string) int {
|
|
value, _ := strconv.Atoi(m.envValue(key))
|
|
return value
|
|
}
|
|
|
|
func valueOrDefaultInt(value int, fallback int) int {
|
|
if value == 0 {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
type localListener struct {
|
|
Proto string
|
|
Address string
|
|
Port int
|
|
PID int
|
|
}
|
|
|
|
func (m Manager) findLocalListeners(port int) []localListener {
|
|
listeners := m.findSSLocalListeners(port)
|
|
if len(listeners) > 0 {
|
|
return listeners
|
|
}
|
|
return m.findProcNetLocalListeners(port)
|
|
}
|
|
|
|
func (m Manager) findSSLocalListeners(port int) []localListener {
|
|
out, err := m.execCommand("ss", "-H", "-lntup")
|
|
if err != nil {
|
|
out, err = m.execCommand("ss", "-lntup")
|
|
}
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var listeners []localListener
|
|
for _, line := range splitLines(out) {
|
|
listener, ok := parseSSListenerLine(line, port)
|
|
if ok {
|
|
listeners = append(listeners, listener)
|
|
}
|
|
}
|
|
return listeners
|
|
}
|
|
|
|
func parseSSListenerLine(line string, port int) (localListener, bool) {
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 {
|
|
return localListener{}, false
|
|
}
|
|
proto := strings.ToLower(fields[0])
|
|
if !strings.HasPrefix(proto, "tcp") && !strings.HasPrefix(proto, "udp") {
|
|
return localListener{}, false
|
|
}
|
|
for _, field := range fields[1:] {
|
|
address, gotPort, ok := parseAddressPortToken(field)
|
|
if !ok || gotPort != port || !isLocalListenerAddress(address) {
|
|
continue
|
|
}
|
|
return localListener{
|
|
Proto: proto,
|
|
Address: address,
|
|
Port: gotPort,
|
|
PID: parseSSPID(line),
|
|
}, true
|
|
}
|
|
return localListener{}, false
|
|
}
|
|
|
|
func parseAddressPortToken(token string) (string, int, bool) {
|
|
token = strings.TrimSpace(token)
|
|
token = strings.Trim(token, "[]")
|
|
if token == "" || strings.HasSuffix(token, ":*") {
|
|
return "", 0, false
|
|
}
|
|
idx := strings.LastIndex(token, ":")
|
|
if idx < 0 || idx == len(token)-1 {
|
|
return "", 0, false
|
|
}
|
|
port, err := strconv.Atoi(token[idx+1:])
|
|
if err != nil {
|
|
return "", 0, false
|
|
}
|
|
address := strings.Trim(token[:idx], "[]")
|
|
if address == "" {
|
|
address = "*"
|
|
}
|
|
return address, port, true
|
|
}
|
|
|
|
func parseSSPID(line string) int {
|
|
const marker = "pid="
|
|
idx := strings.Index(line, marker)
|
|
if idx < 0 {
|
|
return 0
|
|
}
|
|
start := idx + len(marker)
|
|
end := start
|
|
for end < len(line) && line[end] >= '0' && line[end] <= '9' {
|
|
end++
|
|
}
|
|
pid, _ := strconv.Atoi(line[start:end])
|
|
return pid
|
|
}
|
|
|
|
func (m Manager) findProcNetLocalListeners(port int) []localListener {
|
|
var listeners []localListener
|
|
for _, spec := range []struct {
|
|
path string
|
|
proto string
|
|
ipv6 bool
|
|
}{
|
|
{path: "/proc/net/tcp", proto: "tcp", ipv6: false},
|
|
{path: "/proc/net/tcp6", proto: "tcp6", ipv6: true},
|
|
{path: "/proc/net/udp", proto: "udp", ipv6: false},
|
|
{path: "/proc/net/udp6", proto: "udp6", ipv6: true},
|
|
} {
|
|
out, err := m.execCommand("cat", spec.path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, line := range splitLines(out) {
|
|
listener, ok := parseProcNetListenerLine(line, spec.proto, spec.ipv6, port)
|
|
if ok {
|
|
listeners = append(listeners, listener)
|
|
}
|
|
}
|
|
}
|
|
return listeners
|
|
}
|
|
|
|
func parseProcNetListenerLine(line string, proto string, ipv6 bool, port int) (localListener, bool) {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 4 || fields[0] == "sl" {
|
|
return localListener{}, false
|
|
}
|
|
local := fields[1]
|
|
idx := strings.LastIndex(local, ":")
|
|
if idx < 0 || idx == len(local)-1 {
|
|
return localListener{}, false
|
|
}
|
|
gotPort64, err := strconv.ParseInt(local[idx+1:], 16, 32)
|
|
if err != nil || int(gotPort64) != port {
|
|
return localListener{}, false
|
|
}
|
|
state := strings.ToUpper(fields[3])
|
|
if strings.HasPrefix(proto, "tcp") && state != "0A" {
|
|
return localListener{}, false
|
|
}
|
|
addressHex := strings.ToUpper(local[:idx])
|
|
address, ok := procNetLocalAddress(addressHex, ipv6)
|
|
if !ok {
|
|
return localListener{}, false
|
|
}
|
|
return localListener{Proto: proto, Address: address, Port: port}, true
|
|
}
|
|
|
|
func procNetLocalAddress(hexAddress string, ipv6 bool) (string, bool) {
|
|
if !ipv6 {
|
|
switch hexAddress {
|
|
case "00000000":
|
|
return "0.0.0.0", true
|
|
case "0100007F":
|
|
return "127.0.0.1", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
switch hexAddress {
|
|
case "00000000000000000000000000000000":
|
|
return "::", true
|
|
case "00000000000000000000000001000000":
|
|
return "::1", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func isLocalListenerAddress(address string) bool {
|
|
switch strings.ToLower(strings.Trim(address, "[]")) {
|
|
case "*", "0.0.0.0", "::", ":::", "[::]", "127.0.0.1", "::1", "localhost":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (m Manager) processIsOwnedSingBox(pid int) (bool, string) {
|
|
paths := modulecontract.NewPaths(m.dataDir)
|
|
expectedBin := filepathClean(paths.BinDir() + "/sing-box")
|
|
expectedConfig := filepathClean(paths.RenderedConfigDir() + "/singbox.json")
|
|
|
|
exeOut, exeErr := m.execCommand("readlink", fmt.Sprintf("/proc/%d/exe", pid))
|
|
cmdOut, cmdErr := m.execCommand("cat", fmt.Sprintf("/proc/%d/cmdline", pid))
|
|
exe := filepathClean(strings.TrimSpace(exeOut))
|
|
cmdline := filepathClean(strings.ReplaceAll(cmdOut, "\x00", " "))
|
|
cmdTokens := cleanedCmdlineTokens(cmdOut)
|
|
|
|
binOK := exe == expectedBin || stringSliceContains(cmdTokens, expectedBin)
|
|
configOK := stringSliceContains(cmdTokens, expectedConfig)
|
|
if binOK && configOK {
|
|
return true, fmt.Sprintf("exe=%s config=%s", nonEmpty(exe, "<unknown>"), expectedConfig)
|
|
}
|
|
|
|
detailParts := make([]string, 0, 2)
|
|
if exeErr == nil && exe != "" {
|
|
detailParts = append(detailParts, "exe="+exe)
|
|
}
|
|
if cmdErr == nil && cmdline != "" {
|
|
detailParts = append(detailParts, "cmdline="+cmdline)
|
|
}
|
|
if len(detailParts) == 0 {
|
|
return false, "owner unavailable"
|
|
}
|
|
return false, strings.Join(detailParts, " ")
|
|
}
|
|
|
|
func filepathClean(path string) string {
|
|
return strings.TrimRight(strings.TrimSpace(path), "/")
|
|
}
|
|
|
|
func cleanedCmdlineTokens(cmdline string) []string {
|
|
raw := strings.Fields(strings.ReplaceAll(cmdline, "\x00", " "))
|
|
tokens := make([]string, 0, len(raw))
|
|
for _, token := range raw {
|
|
tokens = append(tokens, filepathClean(token))
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
func stringSliceContains(values []string, want string) bool {
|
|
for _, value := range values {
|
|
if value == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nonEmpty(value string, fallback string) string {
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func firstLineContaining(text string, needle string) string {
|
|
for _, line := range splitLines(text) {
|
|
if strings.Contains(line, needle) {
|
|
return strings.TrimSpace(line)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func firstLine(text string) string {
|
|
for _, line := range splitLines(text) {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
return line
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var lines []string
|
|
start := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\n' {
|
|
lines = append(lines, s[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
if start < len(s) {
|
|
lines = append(lines, s[start:])
|
|
}
|
|
return lines
|
|
}
|