143 lines
4.9 KiB
Go
143 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func resolveEndpoint(raw string) (netip.Addr, string, error) {
|
|
host, port, err := net.SplitHostPort(raw)
|
|
if err != nil || host == "" || port == "" {
|
|
return netip.Addr{}, "", errors.New("VPN 公网端点格式无效")
|
|
}
|
|
if ip, err := netip.ParseAddr(host); err == nil {
|
|
if !ip.Is4() || !ip.IsGlobalUnicast() {
|
|
return netip.Addr{}, "", errors.New("VPN 公网端点需要有效 IPv4 地址")
|
|
}
|
|
return ip, port, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
addresses, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", host)
|
|
if err != nil {
|
|
return netip.Addr{}, "", errors.New("无法在连接前解析 VPN 公网端点")
|
|
}
|
|
for _, address := range addresses {
|
|
if address.Is4() && address.IsGlobalUnicast() {
|
|
return address, port, nil
|
|
}
|
|
}
|
|
return netip.Addr{}, "", errors.New("VPN 公网端点没有可用 IPv4 地址")
|
|
}
|
|
|
|
// networkPlan contains only public addressing information. The privileged
|
|
// network adapter still needs to discover the physical gateway and persist a
|
|
// rollback journal before applying any of these routes.
|
|
type networkPlan struct {
|
|
Interface string `json:"interface"`
|
|
ClientAddress netip.Prefix `json:"client_address"`
|
|
Routes []netip.Prefix `json:"routes"`
|
|
Endpoint netip.Addr `json:"endpoint"`
|
|
EndpointBypass bool `json:"endpoint_bypass"`
|
|
PhysicalExit *physicalRoute `json:"physical_exit,omitempty"`
|
|
}
|
|
|
|
type physicalRoute struct {
|
|
Interface string `json:"interface"`
|
|
Gateway *netip.Addr `json:"gateway,omitempty"`
|
|
}
|
|
|
|
// macOS route -n get output is only an observation, never an executable
|
|
// command. Reject another VPN's utun as the physical endpoint exit.
|
|
func parsePhysicalRoute(output string) (physicalRoute, error) {
|
|
var exit physicalRoute
|
|
for _, line := range strings.Split(output, "\n") {
|
|
key, value, ok := strings.Cut(strings.TrimSpace(line), ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
value = strings.TrimSpace(value)
|
|
switch key {
|
|
case "interface":
|
|
exit.Interface = value
|
|
case "gateway":
|
|
gateway, err := netip.ParseAddr(value)
|
|
if err != nil || !gateway.Is4() || gateway.IsUnspecified() || gateway.IsLoopback() {
|
|
return physicalRoute{}, errors.New("物理网关地址无效")
|
|
}
|
|
exit.Gateway = &gateway
|
|
}
|
|
}
|
|
if len(exit.Interface) < 2 || len(exit.Interface) > 16 || strings.HasPrefix(exit.Interface, "utun") || exit.Interface == "lo0" {
|
|
return physicalRoute{}, errors.New("VPN 网关当前未经过物理网络出口")
|
|
}
|
|
for _, char := range exit.Interface {
|
|
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
|
|
return physicalRoute{}, errors.New("物理出口接口名称无效")
|
|
}
|
|
}
|
|
return exit, nil
|
|
}
|
|
|
|
func planNativeRoutes(r *record, interfaceName string, endpoint netip.Addr) (networkPlan, error) {
|
|
if !strings.HasPrefix(interfaceName, "utun") || len(interfaceName) < 5 {
|
|
return networkPlan{}, errors.New("本机接口名称无效")
|
|
}
|
|
for _, digit := range interfaceName[4:] {
|
|
if digit < '0' || digit > '9' {
|
|
return networkPlan{}, errors.New("本机接口名称无效")
|
|
}
|
|
}
|
|
address, err := netip.ParsePrefix(r.Config.ClientAddress)
|
|
if err != nil || !address.Addr().Is4() || address.Addr().IsUnspecified() {
|
|
return networkPlan{}, errors.New("本机 IPv4 地址无效")
|
|
}
|
|
if !endpoint.IsValid() || !endpoint.Is4() || !endpoint.IsGlobalUnicast() {
|
|
return networkPlan{}, errors.New("VPN 公网端点必须解析为可达的 IPv4 地址")
|
|
}
|
|
plan := networkPlan{Interface: interfaceName, ClientAddress: address, Endpoint: endpoint, Routes: []netip.Prefix{}}
|
|
seen := make(map[netip.Prefix]bool)
|
|
for _, part := range strings.Split(r.Config.AllowedIPs, ",") {
|
|
prefix, err := netip.ParsePrefix(strings.TrimSpace(part))
|
|
if err != nil {
|
|
return networkPlan{}, errors.New("WireGuard 授权路由无效")
|
|
}
|
|
if !prefix.Addr().Is4() {
|
|
return networkPlan{}, errors.New("IPv6 系统路由尚未实现,请在专线中仅授权 IPv4 路由")
|
|
}
|
|
prefix = prefix.Masked()
|
|
if prefix.Bits() == 0 {
|
|
// These /1 routes override the physical default while retaining it
|
|
// for explicit endpoint /32 bypass. The helper must install the bypass.
|
|
for _, half := range []string{"0.0.0.0/1", "128.0.0.0/1"} {
|
|
p := netip.MustParsePrefix(half)
|
|
if !seen[p] {
|
|
plan.Routes = append(plan.Routes, p)
|
|
seen[p] = true
|
|
}
|
|
}
|
|
plan.EndpointBypass = true
|
|
continue
|
|
}
|
|
if !seen[prefix] {
|
|
plan.Routes = append(plan.Routes, prefix)
|
|
seen[prefix] = true
|
|
}
|
|
if prefix.Contains(endpoint) {
|
|
plan.EndpointBypass = true
|
|
}
|
|
}
|
|
if plan.EndpointBypass {
|
|
// A physical /32 must be installed before any matching VPN route.
|
|
// Otherwise a full-tunnel VPN can attempt to reach itself through TUN.
|
|
if endpoint == address.Addr() {
|
|
return networkPlan{}, errors.New("VPN 端点与本机地址冲突")
|
|
}
|
|
}
|
|
return plan, nil
|
|
}
|