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
308 lines
7.4 KiB
Go
308 lines
7.4 KiB
Go
package core
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.zapret.moe/zapretdiscordyoutube/RKNnoVPN/runtime/internal/config"
|
|
)
|
|
|
|
var procNetTCPFiles = []string{"/proc/net/tcp", "/proc/net/tcp6"}
|
|
|
|
// VerifyChainedProxyOwnerPackages checks that declared loopback proxy owners
|
|
// resolve to Android UIDs and, when a matching port is already listening, that
|
|
// the listener belongs to the declared package.
|
|
func VerifyChainedProxyOwnerPackages(cfg *config.Config) error {
|
|
profiles := localOutboundProxyProfiles(cfg)
|
|
if len(profiles) == 0 {
|
|
return nil
|
|
}
|
|
ports := make([]int, 0, len(profiles))
|
|
for _, profile := range profiles {
|
|
if strings.TrimSpace(profile.OwnerPackage) != "" {
|
|
ports = append(ports, profile.Port)
|
|
}
|
|
}
|
|
if len(ports) == 0 {
|
|
return nil
|
|
}
|
|
owners := tcpListenerOwnersByPort(ports)
|
|
var problems []string
|
|
for _, profile := range profiles {
|
|
ownerPackage := strings.TrimSpace(profile.OwnerPackage)
|
|
if ownerPackage == "" {
|
|
continue
|
|
}
|
|
resolution := ResolvePackageUIDsDetailed([]string{ownerPackage})
|
|
expected := map[int]bool{}
|
|
for _, uid := range resolution.UIDs {
|
|
parsed, err := strconv.Atoi(uid)
|
|
if err == nil {
|
|
expected[parsed] = true
|
|
}
|
|
}
|
|
if len(expected) == 0 {
|
|
problems = append(problems, fmt.Sprintf("local proxy port %d owner package %s did not resolve to a UID", profile.Port, ownerPackage))
|
|
continue
|
|
}
|
|
for _, actualUID := range owners[profile.Port] {
|
|
if !expected[actualUID] {
|
|
problems = append(problems, fmt.Sprintf("local proxy port %d is owned by UID %d, expected package %s", profile.Port, actualUID, ownerPackage))
|
|
}
|
|
}
|
|
}
|
|
if len(problems) > 0 {
|
|
return errors.New(strings.Join(problems, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildChainedProxyProtectionEnv returns ports of local proxy outbounds and
|
|
// the owner UIDs that may reach those ports. The third return value contains
|
|
// per-port allow rules in "port:uid" form for stricter iptables rendering.
|
|
func BuildChainedProxyProtectionEnv(cfg *config.Config) (string, string, string) {
|
|
profiles := localOutboundProxyProfiles(cfg)
|
|
if len(profiles) == 0 {
|
|
return "", "", ""
|
|
}
|
|
ports := make([]int, 0, len(profiles))
|
|
for _, profile := range profiles {
|
|
ports = append(ports, profile.Port)
|
|
}
|
|
owners := tcpListenerOwnersByPort(ports)
|
|
|
|
portSet := map[int]bool{}
|
|
uidSet := map[int]bool{}
|
|
portUIDSet := map[int]map[int]bool{}
|
|
addPort := func(port int) {
|
|
if port > 0 {
|
|
portSet[port] = true
|
|
}
|
|
}
|
|
addPortUID := func(port int, uid int) {
|
|
if port <= 0 || uid < 0 {
|
|
return
|
|
}
|
|
addPort(port)
|
|
uidSet[uid] = true
|
|
if portUIDSet[port] == nil {
|
|
portUIDSet[port] = map[int]bool{}
|
|
}
|
|
portUIDSet[port][uid] = true
|
|
}
|
|
|
|
for _, profile := range profiles {
|
|
addPort(profile.Port)
|
|
if ownerPackage := strings.TrimSpace(profile.OwnerPackage); ownerPackage != "" {
|
|
for _, uid := range ResolvePackageUIDsDetailed([]string{ownerPackage}).UIDs {
|
|
parsed, err := strconv.Atoi(uid)
|
|
if err == nil {
|
|
addPortUID(profile.Port, parsed)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
for _, uid := range owners[profile.Port] {
|
|
addPortUID(profile.Port, uid)
|
|
}
|
|
}
|
|
|
|
protectedPorts := make([]int, 0, len(portSet))
|
|
for port := range portSet {
|
|
protectedPorts = append(protectedPorts, port)
|
|
}
|
|
sort.Ints(protectedPorts)
|
|
if len(protectedPorts) == 0 {
|
|
return "", "", ""
|
|
}
|
|
|
|
uids := make([]int, 0, len(uidSet))
|
|
for uid := range uidSet {
|
|
uids = append(uids, uid)
|
|
}
|
|
sort.Ints(uids)
|
|
|
|
rules := make([]string, 0)
|
|
for _, port := range protectedPorts {
|
|
portUIDs := make([]int, 0, len(portUIDSet[port]))
|
|
for uid := range portUIDSet[port] {
|
|
portUIDs = append(portUIDs, uid)
|
|
}
|
|
sort.Ints(portUIDs)
|
|
for _, uid := range portUIDs {
|
|
rules = append(rules, fmt.Sprintf("%d:%d", port, uid))
|
|
}
|
|
}
|
|
|
|
return joinInts(protectedPorts), joinInts(uids), strings.Join(rules, " ")
|
|
}
|
|
|
|
func localOutboundProxyProfiles(cfg *config.Config) []*config.NodeProfile {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
profiles := config.ProfilesFromConfigNodes(cfg)
|
|
|
|
reserved := reservedCorePorts(cfg)
|
|
seen := map[int]bool{}
|
|
var result []*config.NodeProfile
|
|
for _, profile := range profiles {
|
|
if profile == nil || profile.Port <= 0 || !isLocalEndpoint(profile.Address) {
|
|
continue
|
|
}
|
|
if reserved[profile.Port] || seen[profile.Port] {
|
|
continue
|
|
}
|
|
seen[profile.Port] = true
|
|
result = append(result, profile)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return result[i].Port < result[j].Port
|
|
})
|
|
return result
|
|
}
|
|
|
|
func localOutboundProxyPorts(cfg *config.Config) []int {
|
|
profiles := localOutboundProxyProfiles(cfg)
|
|
ports := make([]int, 0, len(profiles))
|
|
for _, profile := range profiles {
|
|
ports = append(ports, profile.Port)
|
|
}
|
|
return ports
|
|
}
|
|
|
|
func reservedCorePorts(cfg *config.Config) map[int]bool {
|
|
ports := map[int]bool{}
|
|
if cfg == nil {
|
|
return ports
|
|
}
|
|
tproxyPort := cfg.Proxy.TProxyPort
|
|
if tproxyPort == 0 {
|
|
tproxyPort = 10853
|
|
}
|
|
dnsPort := cfg.Proxy.DNSPort
|
|
if dnsPort == 0 {
|
|
dnsPort = 10856
|
|
}
|
|
for _, port := range []int{tproxyPort, dnsPort, cfg.Proxy.APIPort} {
|
|
if port > 0 {
|
|
ports[port] = true
|
|
}
|
|
}
|
|
profileInbounds := cfg.ResolveProfileInbounds()
|
|
for _, port := range []int{profileInbounds.SocksPort, profileInbounds.HTTPPort} {
|
|
if port > 0 {
|
|
ports[port] = true
|
|
}
|
|
}
|
|
return ports
|
|
}
|
|
|
|
func isLocalEndpoint(address string) bool {
|
|
host := strings.ToLower(strings.TrimSpace(address))
|
|
host = strings.Trim(host, "[]")
|
|
if host == "" {
|
|
return false
|
|
}
|
|
switch host {
|
|
case "localhost", "ip6-localhost":
|
|
return true
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return ip != nil && (ip.IsLoopback() || ip.IsUnspecified())
|
|
}
|
|
|
|
func tcpListenerOwnersByPort(ports []int) map[int][]int {
|
|
wanted := map[int]bool{}
|
|
for _, port := range ports {
|
|
if port > 0 {
|
|
wanted[port] = true
|
|
}
|
|
}
|
|
owners := map[int]map[int]bool{}
|
|
for _, path := range procNetTCPFiles {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for port, uids := range parseProcNetTCPListeners(string(data), wanted) {
|
|
if owners[port] == nil {
|
|
owners[port] = map[int]bool{}
|
|
}
|
|
for _, uid := range uids {
|
|
owners[port][uid] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
result := map[int][]int{}
|
|
for port, set := range owners {
|
|
uids := make([]int, 0, len(set))
|
|
for uid := range set {
|
|
uids = append(uids, uid)
|
|
}
|
|
sort.Ints(uids)
|
|
result[port] = uids
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseProcNetTCPListeners(raw string, wanted map[int]bool) map[int][]int {
|
|
result := map[int]map[int]bool{}
|
|
for _, line := range strings.Split(raw, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 10 || fields[0] == "sl" || fields[3] != "0A" {
|
|
continue
|
|
}
|
|
port, err := parseProcNetPort(fields[1])
|
|
if err != nil || !wanted[port] {
|
|
continue
|
|
}
|
|
uid, err := strconv.Atoi(fields[7])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if result[port] == nil {
|
|
result[port] = map[int]bool{}
|
|
}
|
|
result[port][uid] = true
|
|
}
|
|
out := map[int][]int{}
|
|
for port, set := range result {
|
|
uids := make([]int, 0, len(set))
|
|
for uid := range set {
|
|
uids = append(uids, uid)
|
|
}
|
|
sort.Ints(uids)
|
|
out[port] = uids
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseProcNetPort(localAddress string) (int, error) {
|
|
parts := strings.Split(localAddress, ":")
|
|
if len(parts) < 2 {
|
|
return 0, fmt.Errorf("missing port in %q", localAddress)
|
|
}
|
|
port64, err := strconv.ParseInt(parts[len(parts)-1], 16, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return int(port64), nil
|
|
}
|
|
|
|
func joinInts(values []int) string {
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
parts = append(parts, strconv.Itoa(value))
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|