Files
Zakk 1dd2bf28ad feat(service): the tun transparent proxy is the core's own device
transparentType=tun no longer starts the TinyTun process. The template
emits a tun-mips inbound tagged transparent — device name, address, MTU,
the DNS module's address as the relay target, the user's exclusion list
plus v2rayA and the core by name and v2rayA by PID — and the service does
what the operating system needs once the core has the device up.

On Linux that is one table and four policy rules: table 2020 holds the
default route into tun0; marked packets (0x80: the core's sockets, the
direct outbound, the DNS module's upstreams) look up main in full and
leave through the physical interface, or fail with ENETUNREACH when main
has no route for their family rather than fall into the TUN; unmarked
packets take main with its default route suppressed, so LAN and static
routes stay physical, and everything else goes to table 2020. The main
table is never edited, so stopping restores egress by itself. A rule in
our preference range or a route in our table that is not ours is reported
and left alone; our own leftovers from a crash are removed first. Every
command that succeeded is recorded and undone in reverse on stop.

Three things the TinyTun path never hit are now on tun's path, so they
are fixed here: the DNS module is generated in tun mode (TinyTun did DNS
itself); its 127.2.0.17:53 listener is only added when port 53 is free,
instead of making the core exit on a host running another resolver; its
primary listener stays on loopback unless port sharing is on or the user
chose an address, and the DNS REDIRECT rules and their cleanup follow the
module's port instead of a hard-coded 52353, and a ::1 listener is added
when IPv6 is supported, because ip6tables REDIRECT delivers an
application's IPv6 query to ::1 which the IPv4 listener never saw. The
tun branch no longer
skips those rules: they are what catches queries to a LAN resolver, which
the routing keeps off the TUN.

The connectivity monitor probes the core's API port; the TinyTun socks
loop-back port it used is gone. Removed with TinyTun: --tinytun-bin, the
tunProcessBackend (eBPF) and tunBypassInterfaces settings (connected and
static routes bypass the TUN unconditionally now), the /networkInterfaces
endpoint that only served the latter, and the build tag. The version
endpoint reports tunSupported instead of tinytunSupported.

Tests cover the generated inbound and DNS module, the listen-address
rules, the redirect port, and — with V2RAYA_CORE_BIN set — that the
generated document loads in the real core. Windows and macOS route
handling follows in a separate commit; until then those builds report
the TUN as unsupported.
2026-09-19 10:34:55 +10:00

215 lines
6.9 KiB
Go

package v2ray
import (
"fmt"
"net"
"net/netip"
"os/exec"
"strings"
"time"
"github.com/v2rayA/v2rayA/kernel/iptables"
"github.com/v2rayA/v2rayA/pkg/util/log"
)
const (
tunSupported = true
// tunBindsEgress says the core binds its sockets to the physical
// interface (no socket mark here).
tunBindsEgress = false
tunDeviceName = "tun0"
// tunRouteTable holds the default route into the device; the rules
// below send everything there that the main table cannot place on a
// more specific route. Marked packets (0x80: the core's own sockets,
// the direct outbound, the DNS module's upstreams) look up main in
// full and leave through the physical interface, or fail with
// ENETUNREACH rather than fall into the TUN when main has no route
// for their family.
tunRouteTable = "2020"
tunPrefMarked = "7000"
tunPrefUnreach = "7001"
tunPrefNode = "7005"
tunPrefMain = "7010"
tunPrefTun = "7020"
)
// tunRules lists the policy rules as `ip rule show` prints them; the same
// text is used to install, to recognise a leftover as ours, and to delete.
var tunRules = []string{
tunPrefMarked + ":\tfrom all fwmark 0x80/0x80 lookup main",
tunPrefUnreach + ":\tfrom all fwmark 0x80/0x80 unreachable",
tunPrefMain + ":\tfrom all lookup main suppress_prefixlength 0",
tunPrefTun + ":\tfrom all lookup " + tunRouteTable,
}
// tunUndo records the commands whose undo must run on stop, most recent
// last.
var tunUndo []string
func tunInstalled() bool { return len(tunUndo) > 0 }
func tunIPv6Enabled() bool { return iptables.IsIPv6Supported() }
func tunEgressInterface() string { return "" }
func ipCmd(args ...string) (string, error) {
out, err := exec.Command("ip", args...).CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("ip %s: %v: %s", strings.Join(args, " "), err, strings.TrimSpace(string(out)))
}
return string(out), nil
}
// tunConflicts reports rules in our preference range and routes in our
// table that are not ours. Ours are deleted so a crashed previous run does
// not block the next; anything else is the user's and stops the start.
func tunConflicts(family string) error {
out, err := ipCmd(family, "rule", "show")
if err != nil {
return fmt.Errorf("cannot list policy rules: %w", err)
}
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
colon := strings.IndexByte(line, ':')
if colon <= 0 {
continue
}
pref := line[:colon]
if pref < tunPrefMarked || pref > tunPrefTun || len(pref) != 4 {
continue
}
ours := false
for _, r := range tunRules {
if strings.ReplaceAll(line, "\t", " ") == strings.ReplaceAll(r, "\t", " ") {
ours = true
}
}
// Node rules are ours when they name a single host, which is
// how the kernel prints the ones tunRoutesUp installs; a rule
// with a prefix at this preference is somebody else's.
if pref == tunPrefNode && strings.HasSuffix(line, " lookup main") {
if f := strings.Fields(line); len(f) >= 4 && f[len(f)-4] == "to" {
if _, err := netip.ParseAddr(f[len(f)-3]); err == nil {
ours = true
}
}
}
if !ours {
return fmt.Errorf("policy rule %q occupies preference %s reserved for the TUN; remove it or choose another transparent proxy mode", line, pref)
}
// Delete by the full selector, as printed: a bare preference
// would delete whichever rule the kernel lists first there.
args := append([]string{family, "rule", "del", "pref", pref}, strings.Fields(line[colon+1:])...)
if _, err := ipCmd(args...); err != nil {
log.Warn("tun: leftover rule %q: %v", line, err)
}
}
// An IPv6 table that was never populated does not exist yet and the
// listing fails; that is an empty table, not a conflict.
out, err = ipCmd(family, "route", "show", "table", tunRouteTable)
if err != nil {
out = ""
}
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if !strings.HasPrefix(line, "default dev "+tunDeviceName) {
return fmt.Errorf("route %q occupies table %s reserved for the TUN; remove it or choose another transparent proxy mode", line, tunRouteTable)
}
}
_, _ = ipCmd(family, "route", "flush", "table", tunRouteTable)
return nil
}
func tunDevicePresent() bool {
_, err := net.InterfaceByName(tunDeviceName)
return err == nil
}
func tunRoutesUp(_ *Template, nodeIPs []string) error {
tunUndo = nil
families := []string{"-4"}
if tunIPv6Enabled() {
families = append(families, "-6")
}
for _, f := range families {
if err := tunConflicts(f); err != nil {
return err
}
}
// The core creates the device while starting; afterStart runs once its
// API answers, which is normally after that, but do not race it.
deadline := time.Now().Add(5 * time.Second)
for {
if _, err := ipCmd("link", "show", tunDeviceName); err == nil {
break
}
if time.Now().After(deadline) {
return fmt.Errorf("device %s did not appear; the core did not start its TUN inbound", tunDeviceName)
}
time.Sleep(100 * time.Millisecond)
}
for _, f := range families {
steps := [][]string{
{f, "route", "add", "default", "dev", tunDeviceName, "table", tunRouteTable},
{f, "rule", "add", "pref", tunPrefMarked, "fwmark", "0x80/0x80", "lookup", "main"},
{f, "rule", "add", "pref", tunPrefUnreach, "fwmark", "0x80/0x80", "unreachable"},
}
// The proxy nodes themselves stay reachable through the physical
// route for every process (an ssh session to one's own VPS must
// not be sent through that VPS); these sit before the default
// route suppression so the main table's default applies to them.
for _, ip := range nodeIPs {
if parsed := net.ParseIP(ip); parsed == nil || (f == "-6") != (parsed.To4() == nil) {
continue
}
steps = append(steps, []string{f, "rule", "add", "pref", tunPrefNode, "to", ip, "lookup", "main"})
}
steps = append(steps,
[]string{f, "rule", "add", "pref", tunPrefMain, "lookup", "main", "suppress_prefixlength", "0"},
[]string{f, "rule", "add", "pref", tunPrefTun, "lookup", tunRouteTable},
)
for _, step := range steps {
if _, err := ipCmd(step...); err != nil {
tunRoutesDown()
return err
}
tunUndo = append(tunUndo, strings.Join(step, " "))
}
}
log.Info("tun: routes and policy rules installed for %s", tunDeviceName)
return nil
}
// tunRoutesDown undoes the recorded commands in reverse. A failure is
// logged and the rest still run; the device itself vanishes with the core.
func tunRoutesDown() {
for i := len(tunUndo) - 1; i >= 0; i-- {
args := strings.Fields(tunUndo[i])
for j, a := range args {
if a == "add" {
args[j] = "del"
break
}
}
if _, err := ipCmd(args...); err != nil {
log.Warn("tun: undo failed: %v", err)
}
}
tunUndo = nil
}
// tunCleanupResidual removes our rules and table entries left by an
// abnormal exit; it never touches anything that is not exactly ours.
func tunCleanupResidual() {
for _, f := range []string{"-4", "-6"} {
_ = tunConflicts(f)
}
}