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

177 lines
4.2 KiB
Go

package iptables
import (
"fmt"
"net"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"github.com/v2rayA/v2rayA/common"
"github.com/v2rayA/v2rayA/common/cmds"
"github.com/v2rayA/v2rayA/common/parseGeoIP"
"github.com/v2rayA/v2rayA/conf"
"github.com/v2rayA/v2rayA/db/configure"
"golang.org/x/net/nettest"
)
func IPNet2CIDR(ipnet *net.IPNet) string {
ones, _ := ipnet.Mask.Size()
return ipnet.IP.String() + "/" + strconv.Itoa(ones)
}
func GetLocalCIDR() ([]string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil, err
}
var cidrs []string
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok {
cidrs = append(cidrs, IPNet2CIDR(ipnet))
}
}
return cidrs, nil
}
func IsNft() bool {
if _, isNft := Redirect.(*nftRedirect); isNft {
return true
}
return false
}
func IsIPv6Supported() bool {
switch conf.GetEnvironmentConfig().IPV6Support {
case "on":
return true
case "off":
return false
default:
}
if common.IsDocker() {
return false
}
if !nettest.SupportsIPv6() {
return false
} else if runtime.GOOS != "linux" {
return true
}
if IsNft() {
return true
}
return cmds.IsCommandValid("ip6tables") || cmds.IsCommandValid("ip6tables-nft")
}
func IsNftablesSupported() bool {
if runtime.GOOS != "linux" {
return false
}
switch conf.GetEnvironmentConfig().NftablesSupport {
// Warning:
// This is an experimental feature for nftables support.
// The default value is "off" for now but may be changed to "auto" in the future
case "on":
return true
case "off":
return false
default:
}
if common.IsDocker() {
return false
}
if !cmds.IsCommandValid("nft") {
// No nft.
return false
}
out, err := exec.Command("iptables", "--version").Output()
if err != nil {
// No iptables.
return true
}
return strings.Contains(string(out), "nf_tables")
}
func GetWhiteListIPs() ([]string, []string, error) {
dataModal := configure.GetTproxyWhiteIpGroups()
var ipv4List []string
var ipv6List []string
for _, cc := range dataModal.CountryCodes {
ipv4s, ipv6s, _ := parseGeoIP.Parser("geoip.dat", cc)
ipv4List = append(ipv4List, ipv4s...)
ipv6List = append(ipv6List, ipv6s...)
}
for _, v := range dataModal.CustomIps {
// The GUI submits the textarea lines as typed, so a pasted CIDR can
// carry surrounding whitespace that net.ParseCIDR does not accept.
v = strings.TrimSpace(v)
if v == "" {
continue
}
if net.ParseIP(v) == nil {
if _, _, err := net.ParseCIDR(v); err != nil {
return nil, nil, fmt.Errorf("invalid tproxy whitelist IP/CIDR %q", v)
}
}
if strings.Contains(v, ":") {
ipv6List = append(ipv6List, v)
} else {
ipv4List = append(ipv4List, v)
}
}
return ipv4List, ipv6List, nil
}
func IsEnabledTproxyWhiteIpGroups() bool {
ipv4List, ipv6List, err := GetWhiteListIPs()
return err == nil && (len(ipv4List) > 0 || len(ipv6List) > 0)
}
// interface names plus the "*" wildcard users write for docker*/veth*
var interfaceNameRegexp = regexp.MustCompile(`^[A-Za-z0-9_.:*+-]+$`)
func GetExcludedInterfaces() ([]string, error) {
setting := configure.GetSettingNotNil()
if setting.TproxyExcludedInterfaces == "" {
return []string{}, nil
}
ifs := strings.Split(setting.TproxyExcludedInterfaces, ",")
var res []string
for _, v := range ifs {
v = strings.TrimSpace(v)
if v != "" {
if !interfaceNameRegexp.MatchString(v) {
return nil, fmt.Errorf("invalid tproxy excluded interface %q", v)
}
res = append(res, v)
}
}
return res, nil
}
func getTproxySetupValues() (interfaces, ipv4List, ipv6List []string, err error) {
if interfaces, err = GetExcludedInterfaces(); err != nil {
return
}
ipv4List, ipv6List, err = GetWhiteListIPs()
return
}
// DnsModulePort is the port the DNS module listens on: the rule sets are
// written for the historical 52353 and rewritten to the configured port
// when it differs, so the TPROXY and REDIRECT targets, the loop-back RETURN
// and the module's listener always agree.
func DnsModulePort() string {
return configure.GetSettingNotNil().DnsModulePort()
}
func withDnsModulePort(rules string) string {
if port := DnsModulePort(); port != "52353" {
return strings.ReplaceAll(rules, "52353", port)
}
return rules
}