Refine WireGuard client routing and authentication UI
This commit is contained in:
1 parent
eabb3e4472
commit
41fb9d5631
33 files changed
+1709
-469
No files matched your search
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user