mirror of
https://github.com/VaalaCat/frp-panel.git
synced 2025-12-24 11:51:06 +08:00
refactor: rewrite routing rule and do not ping when no allowed ips
This commit is contained in:
@@ -142,6 +142,12 @@ func (w *wireGuard) scheduleVirtualAddrPings(log *logrus.Entry, ifceConfig *defs
|
|||||||
peers := ifceConfig.Peers
|
peers := ifceConfig.Peers
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
p := peer
|
p := peer
|
||||||
|
// 没有 AllowedIPs 的 peer(例如仅用于保持连接/预连接的常驻 peer)不参与 virt addr 探测:
|
||||||
|
// - 此时本机通常没有到对端 virtIP 的路由,探测必然失败
|
||||||
|
// - 若失败写入不可达哨兵,会污染 master 的拓扑 cache,导致 SPF 误判为不可达,进而“完全不连通”
|
||||||
|
if p == nil || len(p.GetAllowedIps()) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
addr := p.GetVirtualIp()
|
addr := p.GetVirtualIp()
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
continue
|
continue
|
||||||
@@ -157,7 +163,10 @@ func (w *wireGuard) scheduleVirtualAddrPings(log *logrus.Entry, ifceConfig *defs
|
|||||||
avg, err := tcpPingAvg(tcpAddr, endpointPingCount, endpointPingTimeout)
|
avg, err := tcpPingAvg(tcpAddr, endpointPingCount, endpointPingTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("failed to tcp ping virt addr %s via %s", addr, tcpAddr)
|
log.WithError(err).Errorf("failed to tcp ping virt addr %s via %s", addr, tcpAddr)
|
||||||
w.storeVirtAddrPing(addr, math.MaxUint32)
|
// 失败时不写入不可达哨兵,避免污染拓扑;删除该条记录即可回退到 endpoint latency
|
||||||
|
if w.virtAddrPingMap != nil {
|
||||||
|
w.virtAddrPingMap.Delete(addr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package wg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,14 +14,28 @@ import (
|
|||||||
"github.com/VaalaCat/frp-panel/pb"
|
"github.com/VaalaCat/frp-panel/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WireGuard 的 AllowedIPs 同时承担两件事:
|
||||||
|
// 1) 出站选路:目的 IP 匹配哪个 peer 的 AllowedIPs,就把包发给哪个 peer
|
||||||
|
// 2) 入站源地址校验:从某 peer 解密出来的 inner packet,其 source IP 必须落在该 peer 的 AllowedIPs
|
||||||
|
//
|
||||||
|
// 因此,多跳转发时,某节点 i 从“上一跳 peer=j”收到的包,其 inner source 仍是“原始源节点 s 的 /32”,
|
||||||
|
// 所以 i 配置 peer(j) 的 AllowedIPs 必须包含这些会经由 j 转发进来的“源地址集合”,否则会直接丢包。
|
||||||
|
// 思路:
|
||||||
|
// - 在一个“对称权重”的图上做最短路(保证路径可逆,避免重复/冲突)
|
||||||
|
// - 同时产出:
|
||||||
|
// - Out(i->nextHop): i 出站时,哪些目的 /32 应走 nextHop(目的集合)
|
||||||
|
// - In(i<-prevHop): i 入站时,从 prevHop 过来的包允许哪些源 /32(源集合)
|
||||||
|
// - 最终对每个 i 的每个直连 peer(j),AllowedIPs = Out(i->j) ∪ In(i<-j)
|
||||||
|
// - 严格校验:对同一节点 i,不允许出现同一个 /32 同时出现在多个 peer 的 AllowedIPs(否则 WG 行为不确定)
|
||||||
|
|
||||||
type AllowedIPsPlanner interface {
|
type AllowedIPsPlanner interface {
|
||||||
// Compute 基于拓扑与链路指标,计算每个节点应配置到直连邻居的 AllowedIPs。
|
// Compute 基于拓扑与链路指标,计算每个节点应配置到直连邻居的 AllowedIPs。
|
||||||
// 输入的 peers 应包含同一 Network 下的所有 WireGuard 节点,links 为其有向链路。
|
// 输入的 peers 应包含同一 Network 下的所有 WireGuard 节点,links 为其有向链路。
|
||||||
// 返回节点ID->PeerConfig 列表,节点所有 ID->Edge 列表。
|
// 返回:节点ID->PeerConfig 列表,节点ID->Edge 列表(完整候选图,用于展示)。
|
||||||
Compute(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]*pb.WireGuardPeerConfig, map[uint][]Edge, error)
|
Compute(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]*pb.WireGuardPeerConfig, map[uint][]Edge, error)
|
||||||
// BuildGraph 基于拓扑与链路指标,计算每个节点应配置到直连邻居的 AllowedIPs,并返回节点ID->Edge 列表。
|
// BuildGraph 基于拓扑与链路指标,返回完整候选图(用于展示/诊断)。
|
||||||
BuildGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error)
|
BuildGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error)
|
||||||
// BuildFinalGraph 最短路径算法,返回节点ID->Edge 列表。
|
// BuildFinalGraph 返回“最终下发的直连边”与其 routes(用于展示 SPF 结果)。
|
||||||
BuildFinalGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error)
|
BuildFinalGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,37 +56,44 @@ func (p *dijkstraAllowedIPsPlanner) Compute(peers []*models.WireGuard, links []*
|
|||||||
return map[uint][]*pb.WireGuardPeerConfig{}, map[uint][]Edge{}, nil
|
return map[uint][]*pb.WireGuardPeerConfig{}, map[uint][]Edge{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
idToPeer, order := buildNodeIndex(peers)
|
idToPeer, order := buildNodeIndexSorted(peers)
|
||||||
adj := buildAdjacency(order, idToPeer, links, p.policy)
|
cidrByID, err := buildNodeCIDRMap(order, idToPeer)
|
||||||
spfAdj := filterAdjacencyForSPF(order, adj, p.policy)
|
|
||||||
// 路由(AllowedIPs)依赖 WireGuard 的“源地址校验”:下一跳收到的包会按“来自哪个 peer”做匹配,
|
|
||||||
// 并校验 inner packet 的 source IP 是否落在该 peer 的 AllowedIPs 中。
|
|
||||||
// 因此用于承载路由的直连边必须是双向的:若存在单向边,最短路会产生单向选路,导致中间节点丢包。
|
|
||||||
spfAdj = filterAdjacencyForSymmetricLinks(order, spfAdj)
|
|
||||||
aggByNode, edgeInfoMap := runAllPairsDijkstra(order, spfAdj, idToPeer, p.policy)
|
|
||||||
result, err := assemblePeerConfigs(order, aggByNode, edgeInfoMap, idToPeer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
fillIsolates(order, result)
|
|
||||||
if err := ensureRoutingPeerSymmetry(order, result, idToPeer); err != nil {
|
adj := buildAdjacency(order, idToPeer, links, p.policy)
|
||||||
|
// SPF 参与的边:显式边 + 已探测可达的推断边,并要求“可用于转发”的边必须双向存在
|
||||||
|
spfAdj := filterAdjacencyForSPF(order, adj, p.policy)
|
||||||
|
spfAdj = filterAdjacencyForSymmetricLinks(order, spfAdj)
|
||||||
|
|
||||||
|
peerCfgs, finalEdges, err := computeAllowedIPs(order, idToPeer, cidrByID, spfAdj, adj, p.policy)
|
||||||
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填充没有链路的节点
|
// 填充没有链路的节点(展示用)
|
||||||
for _, id := range order {
|
for _, id := range order {
|
||||||
if _, ok := adj[id]; !ok {
|
if _, ok := adj[id]; !ok {
|
||||||
adj[id] = []Edge{}
|
adj[id] = []Edge{}
|
||||||
}
|
}
|
||||||
|
if _, ok := finalEdges[id]; !ok {
|
||||||
|
finalEdges[id] = []Edge{}
|
||||||
|
}
|
||||||
|
if _, ok := peerCfgs[id]; !ok {
|
||||||
|
peerCfgs[id] = []*pb.WireGuardPeerConfig{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, adj, nil
|
return peerCfgs, adj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *dijkstraAllowedIPsPlanner) BuildGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error) {
|
func (p *dijkstraAllowedIPsPlanner) BuildGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error) {
|
||||||
idToPeer, order := buildNodeIndex(peers)
|
if len(peers) == 0 {
|
||||||
|
return map[uint][]Edge{}, nil
|
||||||
|
}
|
||||||
|
idToPeer, order := buildNodeIndexSorted(peers)
|
||||||
adj := buildAdjacency(order, idToPeer, links, p.policy)
|
adj := buildAdjacency(order, idToPeer, links, p.policy)
|
||||||
// 填充没有链路的节点
|
|
||||||
for _, id := range order {
|
for _, id := range order {
|
||||||
if _, ok := adj[id]; !ok {
|
if _, ok := adj[id]; !ok {
|
||||||
adj[id] = []Edge{}
|
adj[id] = []Edge{}
|
||||||
@@ -80,56 +103,39 @@ func (p *dijkstraAllowedIPsPlanner) BuildGraph(peers []*models.WireGuard, links
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *dijkstraAllowedIPsPlanner) BuildFinalGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error) {
|
func (p *dijkstraAllowedIPsPlanner) BuildFinalGraph(peers []*models.WireGuard, links []*models.WireGuardLink) (map[uint][]Edge, error) {
|
||||||
idToPeer, order := buildNodeIndex(peers)
|
if len(peers) == 0 {
|
||||||
|
return map[uint][]Edge{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idToPeer, order := buildNodeIndexSorted(peers)
|
||||||
|
cidrByID, err := buildNodeCIDRMap(order, idToPeer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
adj := buildAdjacency(order, idToPeer, links, p.policy)
|
adj := buildAdjacency(order, idToPeer, links, p.policy)
|
||||||
spfAdj := filterAdjacencyForSPF(order, adj, p.policy)
|
spfAdj := filterAdjacencyForSPF(order, adj, p.policy)
|
||||||
spfAdj = filterAdjacencyForSymmetricLinks(order, spfAdj)
|
spfAdj = filterAdjacencyForSymmetricLinks(order, spfAdj)
|
||||||
routesInfoMap, edgeInfoMap := runAllPairsDijkstra(order, spfAdj, idToPeer, p.policy)
|
|
||||||
|
|
||||||
ret := map[uint][]Edge{}
|
_, finalEdges, err := computeAllowedIPs(order, idToPeer, cidrByID, spfAdj, adj, p.policy)
|
||||||
for src, edgeInfo := range edgeInfoMap {
|
if err != nil {
|
||||||
for next := range edgeInfo {
|
return nil, err
|
||||||
if _, ok := adj[src]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
originEdge := Edge{}
|
|
||||||
finded := false
|
|
||||||
for _, e := range adj[src] {
|
|
||||||
if e.to == next {
|
|
||||||
originEdge = e
|
|
||||||
finded = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !finded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
routesInfo := routesInfoMap[src][next]
|
|
||||||
|
|
||||||
ret[src] = append(ret[src], Edge{
|
|
||||||
to: next,
|
|
||||||
latency: originEdge.latency,
|
|
||||||
upMbps: originEdge.upMbps,
|
|
||||||
toEndpoint: originEdge.toEndpoint,
|
|
||||||
routes: lo.Keys(routesInfo),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, id := range order {
|
for _, id := range order {
|
||||||
if _, ok := ret[id]; !ok {
|
if _, ok := finalEdges[id]; !ok {
|
||||||
ret[id] = []Edge{}
|
finalEdges[id] = []Edge{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret, nil
|
return finalEdges, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edge 表示候选/最终图里的“有向直连边”。
|
||||||
type Edge struct {
|
type Edge struct {
|
||||||
to uint
|
to uint
|
||||||
latency uint32
|
latency uint32
|
||||||
upMbps uint32
|
upMbps uint32
|
||||||
toEndpoint *models.Endpoint // 指定的目标端点,可能为 nil
|
toEndpoint *models.Endpoint // 指定的目标端点,可能为 nil
|
||||||
routes []string // 路由信息
|
routes []string // 最终展示:该直连 peer 承载的路由(AllowedIPs)
|
||||||
explicit bool // true: 显式 link;false: 推断/探测用 link
|
explicit bool // true: 显式 link;false: 推断/探测用 link
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,45 +153,24 @@ func (e *Edge) ToPB() *pb.WireGuardLink {
|
|||||||
return link
|
return link
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterAdjacencyForSymmetricLinks 仅保留“存在反向直连边”的邻接(用于 SPF)。
|
// buildNodeIndexSorted 返回:id->peer 映射 与 按 id 排序的 order(用于确定性)
|
||||||
// 这样最短路产生的每一步转发 hop 都对应一个双向直连 peer,避免出现单向路由导致的丢包。
|
func buildNodeIndexSorted(peers []*models.WireGuard) (map[uint]*models.WireGuard, []uint) {
|
||||||
func filterAdjacencyForSymmetricLinks(order []uint, adj map[uint][]Edge) map[uint][]Edge {
|
idToPeer := make(map[uint]*models.WireGuard, len(peers))
|
||||||
ret := make(map[uint][]Edge, len(order))
|
order := make([]uint, 0, len(peers))
|
||||||
edgeSet := make(map[[2]uint]struct{}, 16)
|
for _, p := range peers {
|
||||||
|
if p == nil {
|
||||||
for from, edges := range adj {
|
continue
|
||||||
for _, e := range edges {
|
|
||||||
edgeSet[[2]uint{from, e.to}] = struct{}{}
|
|
||||||
}
|
}
|
||||||
|
id := uint(p.ID)
|
||||||
|
idToPeer[id] = p
|
||||||
|
order = append(order, id)
|
||||||
}
|
}
|
||||||
|
sort.Slice(order, func(i, j int) bool { return order[i] < order[j] })
|
||||||
for from, edges := range adj {
|
return idToPeer, order
|
||||||
for _, e := range edges {
|
|
||||||
if _, ok := edgeSet[[2]uint{e.to, from}]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ret[from] = append(ret[from], e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, id := range order {
|
|
||||||
if _, ok := ret[id]; !ok {
|
|
||||||
ret[id] = []Edge{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureRoutingPeerSymmetry 确保:如果 src 的 peers 中存在 nextHop(承载路由),则 nextHop 的 peers 中也必须存在 src。
|
func buildNodeCIDRMap(order []uint, idToPeer map[uint]*models.WireGuard) (map[uint]string, error) {
|
||||||
// 这里“对称”不是指两端 routes/AllowedIPs 集合一致,而是指两端都必须配置对方这个 peer,
|
out := make(map[uint]string, len(order))
|
||||||
// 以满足 WG 的解密与源地址校验(否则 nextHop 会丢弃来自 src 的转发包)。
|
|
||||||
func ensureRoutingPeerSymmetry(order []uint, peerCfgs map[uint][]*pb.WireGuardPeerConfig, idToPeer map[uint]*models.WireGuard) error {
|
|
||||||
if len(order) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预计算每个节点自身的 /32 CIDR(AsBasePeerConfig 返回的 AllowedIps[0])
|
|
||||||
selfCIDR := make(map[uint]string, len(order))
|
|
||||||
for _, id := range order {
|
for _, id := range order {
|
||||||
p := idToPeer[id]
|
p := idToPeer[id]
|
||||||
if p == nil {
|
if p == nil {
|
||||||
@@ -193,108 +178,66 @@ func ensureRoutingPeerSymmetry(order []uint, peerCfgs map[uint][]*pb.WireGuardPe
|
|||||||
}
|
}
|
||||||
base, err := p.AsBasePeerConfig(nil)
|
base, err := p.AsBasePeerConfig(nil)
|
||||||
if err != nil || len(base.GetAllowedIps()) == 0 {
|
if err != nil || len(base.GetAllowedIps()) == 0 {
|
||||||
continue
|
return nil, fmt.Errorf("invalid wireguard local address for id=%d", id)
|
||||||
}
|
}
|
||||||
selfCIDR[id] = base.GetAllowedIps()[0]
|
out[id] = base.GetAllowedIps()[0]
|
||||||
}
|
}
|
||||||
|
return out, nil
|
||||||
hasPeer := func(owner uint, peerID uint) bool {
|
|
||||||
for _, pc := range peerCfgs[owner] {
|
|
||||||
if pc == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if uint(pc.GetId()) == peerID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, src := range order {
|
|
||||||
for _, pc := range peerCfgs[src] {
|
|
||||||
if pc == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(pc.GetAllowedIps()) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nextHop := uint(pc.GetId())
|
|
||||||
if nextHop == 0 || nextHop == src {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if hasPeer(nextHop, src) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
remote := idToPeer[src]
|
|
||||||
if remote == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
base, err := remote.AsBasePeerConfig(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cidr := selfCIDR[src]; cidr != "" {
|
|
||||||
base.AllowedIps = []string{cidr}
|
|
||||||
}
|
|
||||||
peerCfgs[nextHop] = append(peerCfgs[nextHop], base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, id := range order {
|
|
||||||
sort.SliceStable(peerCfgs[id], func(i, j int) bool {
|
|
||||||
return peerCfgs[id][i].GetClientId() < peerCfgs[id][j].GetClientId()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildNodeIndex(peers []*models.WireGuard) (map[uint]*models.WireGuard, []uint) {
|
|
||||||
idToPeer := make(map[uint]*models.WireGuard, len(peers))
|
|
||||||
order := make([]uint, 0, len(peers))
|
|
||||||
for _, p := range peers {
|
|
||||||
idToPeer[uint(p.ID)] = p
|
|
||||||
order = append(order, uint(p.ID))
|
|
||||||
}
|
|
||||||
return idToPeer, order
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildAdjacency 构建“候选直连边”:
|
||||||
|
// 1) 显式链路(管理员配置)直接加入
|
||||||
|
// 2) 若某节点具备 endpoint,则其他节点可按 ACL 推断直连它(用于探测/候选)
|
||||||
func buildAdjacency(order []uint, idToPeer map[uint]*models.WireGuard, links []*models.WireGuardLink, policy RoutingPolicy) map[uint][]Edge {
|
func buildAdjacency(order []uint, idToPeer map[uint]*models.WireGuard, links []*models.WireGuardLink, policy RoutingPolicy) map[uint][]Edge {
|
||||||
adj := make(map[uint][]Edge, len(order))
|
adj := make(map[uint][]Edge, len(order))
|
||||||
|
|
||||||
|
online := func(id uint) bool {
|
||||||
|
if policy.CliMgr == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p := idToPeer[id]
|
||||||
|
if p == nil || p.ClientID == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lastSeenAt, ok := policy.CliMgr.GetLastSeenAt(p.ClientID)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if policy.OfflineThreshold > 0 && time.Since(lastSeenAt) > policy.OfflineThreshold {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// 1) 显式链路
|
// 1) 显式链路
|
||||||
for _, l := range links {
|
for _, l := range links {
|
||||||
if !l.Active {
|
if l == nil || !l.Active {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
from := l.FromWireGuardID
|
from := l.FromWireGuardID
|
||||||
to := l.ToWireGuardID
|
to := l.ToWireGuardID
|
||||||
|
|
||||||
if _, ok := idToPeer[from]; !ok {
|
if _, ok := idToPeer[from]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := idToPeer[to]; !ok {
|
if _, ok := idToPeer[to]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !online(from) || !online(to) {
|
||||||
if lastSeenAt, ok := policy.CliMgr.GetLastSeenAt(idToPeer[from].ClientID); !ok || time.Since(lastSeenAt) > policy.OfflineThreshold {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 如果两个 peer 都没有 endpoint,则不建立链路(无法直连)
|
||||||
if lastSeenAt, ok := policy.CliMgr.GetLastSeenAt(idToPeer[to].ClientID); !ok || time.Since(lastSeenAt) > policy.OfflineThreshold {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果两个peer都没有endpoint,则不建立链路
|
|
||||||
if len(idToPeer[from].AdvertisedEndpoints) == 0 && len(idToPeer[to].AdvertisedEndpoints) == 0 {
|
if len(idToPeer[from].AdvertisedEndpoints) == 0 && len(idToPeer[to].AdvertisedEndpoints) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
latency := l.LatencyMs
|
latency := l.LatencyMs
|
||||||
if latency == 0 { // 如果指定latency为0,则使用真实值
|
if latency == 0 {
|
||||||
if latencyMs, ok := policy.NetworkTopologyCache.GetLatencyMs(from, to); ok {
|
if policy.NetworkTopologyCache != nil {
|
||||||
latency = latencyMs
|
if latencyMs, ok := policy.NetworkTopologyCache.GetLatencyMs(from, to); ok {
|
||||||
} else {
|
latency = latencyMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if latency == 0 {
|
||||||
latency = policy.DefaultEndpointLatencyMs
|
latency = policy.DefaultEndpointLatencyMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,18 +251,17 @@ func buildAdjacency(order []uint, idToPeer map[uint]*models.WireGuard, links []*
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) 若某节点具备 endpoint,则所有其他节点可直连它
|
// 2) 推断/探测用边:若某节点具备 endpoint,则所有其他节点可直连它
|
||||||
edgeSet := make(map[[2]uint]struct{}, 16)
|
edgeSet := make(map[[2]uint]struct{}, 64)
|
||||||
for from, edges := range adj {
|
for from, edges := range adj {
|
||||||
for _, e := range edges { // 先拿到所有直连的节点
|
for _, e := range edges {
|
||||||
edgeSet[[2]uint{from, e.to}] = struct{}{}
|
edgeSet[[2]uint{from, e.to}] = struct{}{}
|
||||||
edgeSet[[2]uint{e.to, from}] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, to := range order {
|
for _, to := range order {
|
||||||
peer := idToPeer[to]
|
peerTo := idToPeer[to]
|
||||||
if peer == nil || len(peer.AdvertisedEndpoints) == 0 {
|
if peerTo == nil || len(peerTo.AdvertisedEndpoints) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, from := range order {
|
for _, from := range order {
|
||||||
@@ -329,83 +271,96 @@ func buildAdjacency(order []uint, idToPeer map[uint]*models.WireGuard, links []*
|
|||||||
if _, ok := idToPeer[from]; !ok {
|
if _, ok := idToPeer[from]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !online(from) || !online(to) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
latency := policy.DefaultEndpointLatencyMs
|
latency := policy.DefaultEndpointLatencyMs
|
||||||
// GetLatencyMs 已自带“正反向兜底 + endpoint/virt ping 组合”,这里避免重复查询与覆盖,减少抖动
|
if policy.NetworkTopologyCache != nil {
|
||||||
if latencyMs, ok := policy.NetworkTopologyCache.GetLatencyMs(from, to); ok {
|
if latencyMs, ok := policy.NetworkTopologyCache.GetLatencyMs(from, to); ok {
|
||||||
latency = latencyMs
|
latency = latencyMs
|
||||||
}
|
|
||||||
|
|
||||||
if lastSeenAt, ok := policy.CliMgr.GetLastSeenAt(idToPeer[from].ClientID); !ok || time.Since(lastSeenAt) > policy.OfflineThreshold {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastSeenAt, ok := policy.CliMgr.GetLastSeenAt(idToPeer[to].ClientID); !ok || time.Since(lastSeenAt) > policy.OfflineThreshold {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有 acl 限制
|
|
||||||
if policy.ACL.CanConnect(idToPeer[from], idToPeer[to]) {
|
|
||||||
key1 := [2]uint{from, to}
|
|
||||||
if _, exists := edgeSet[key1]; exists {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adj[from] = append(adj[from], Edge{
|
|
||||||
to: to,
|
|
||||||
latency: latency,
|
|
||||||
upMbps: policy.DefaultEndpointUpMbps,
|
|
||||||
explicit: false,
|
|
||||||
})
|
|
||||||
edgeSet[key1] = struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if policy.ACL.CanConnect(idToPeer[to], idToPeer[from]) {
|
// 注意:推断边需要按“两个方向”分别判断 ACL 并分别建边。
|
||||||
key2 := [2]uint{to, from}
|
// 这样即使 from 没有 endpoint,也能被 endpoint 节点纳入邻接(满足对称直连 peer 的要求)。
|
||||||
if _, exists := edgeSet[key2]; exists {
|
|
||||||
continue
|
// from -> to
|
||||||
|
if policy.ACL == nil || policy.ACL.CanConnect(idToPeer[from], idToPeer[to]) {
|
||||||
|
key := [2]uint{from, to}
|
||||||
|
if _, exists := edgeSet[key]; !exists {
|
||||||
|
adj[from] = append(adj[from], Edge{
|
||||||
|
to: to,
|
||||||
|
latency: latency,
|
||||||
|
upMbps: policy.DefaultEndpointUpMbps,
|
||||||
|
explicit: false,
|
||||||
|
})
|
||||||
|
edgeSet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// to -> from(反向边同样使用同一对节点的 latency 估计;GetLatencyMs 本身已做正反向兜底)
|
||||||
|
if policy.ACL == nil || policy.ACL.CanConnect(idToPeer[to], idToPeer[from]) {
|
||||||
|
key := [2]uint{to, from}
|
||||||
|
if _, exists := edgeSet[key]; !exists {
|
||||||
|
adj[to] = append(adj[to], Edge{
|
||||||
|
to: from,
|
||||||
|
latency: latency,
|
||||||
|
upMbps: policy.DefaultEndpointUpMbps,
|
||||||
|
explicit: false,
|
||||||
|
})
|
||||||
|
edgeSet[key] = struct{}{}
|
||||||
}
|
}
|
||||||
adj[to] = append(adj[to], Edge{
|
|
||||||
to: from,
|
|
||||||
latency: latency,
|
|
||||||
upMbps: policy.DefaultEndpointUpMbps,
|
|
||||||
explicit: false,
|
|
||||||
})
|
|
||||||
edgeSet[key2] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 稳定排序:保证遍历顺序确定性
|
||||||
|
for _, from := range order {
|
||||||
|
if edges, ok := adj[from]; ok {
|
||||||
|
sort.SliceStable(edges, func(i, j int) bool {
|
||||||
|
if edges[i].explicit != edges[j].explicit {
|
||||||
|
return edges[i].explicit // explicit 优先
|
||||||
|
}
|
||||||
|
return edges[i].to < edges[j].to
|
||||||
|
})
|
||||||
|
adj[from] = edges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return adj
|
return adj
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterAdjacencyForSPF 将“用于探测的候选邻接(adj)”过滤为“允许进入 SPF 的邻接”。
|
func isUnreachableLatency(latency uint32) bool {
|
||||||
//
|
// 兼容两类不可达哨兵:
|
||||||
// 参考 OSPF:新邻接必须先被确认可达(这里用 runtime ping/virt ping 的存在性作为信号)后,
|
// - math.MaxUint32(历史实现)
|
||||||
// 才能参与最短路计算。否则在节点刚更新/刚加入时,会因为默认权重过低被误选,导致部分节点不可达。
|
// - math.MaxInt32(部分展示/转换链路里会出现 2147483647)
|
||||||
|
return latency == math.MaxUint32 || latency == uint32(math.MaxInt32)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterAdjacencyForSPF:显式边直接保留;推断边必须有探测数据,且不可达哨兵值剔除
|
||||||
func filterAdjacencyForSPF(order []uint, adj map[uint][]Edge, policy RoutingPolicy) map[uint][]Edge {
|
func filterAdjacencyForSPF(order []uint, adj map[uint][]Edge, policy RoutingPolicy) map[uint][]Edge {
|
||||||
ret := make(map[uint][]Edge, len(order))
|
ret := make(map[uint][]Edge, len(order))
|
||||||
|
|
||||||
for from, edges := range adj {
|
for from, edges := range adj {
|
||||||
for _, e := range edges {
|
for _, e := range edges {
|
||||||
// 显式 link:管理员配置的边,允许进入 SPF
|
|
||||||
if e.explicit {
|
if e.explicit {
|
||||||
ret[from] = append(ret[from], e)
|
ret[from] = append(ret[from], e)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if policy.NetworkTopologyCache == nil {
|
||||||
// 推断/探测用 link:必须已存在探测数据,且不可达哨兵值要剔除
|
continue
|
||||||
|
}
|
||||||
latency, ok := policy.NetworkTopologyCache.GetLatencyMs(from, e.to)
|
latency, ok := policy.NetworkTopologyCache.GetLatencyMs(from, e.to)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if latency == math.MaxUint32 {
|
if isUnreachableLatency(latency) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
e.latency = latency
|
e.latency = latency
|
||||||
ret[from] = append(ret[from], e)
|
ret[from] = append(ret[from], e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range order {
|
for _, id := range order {
|
||||||
if _, ok := ret[id]; !ok {
|
if _, ok := ret[id]; !ok {
|
||||||
ret[id] = []Edge{}
|
ret[id] = []Edge{}
|
||||||
@@ -414,78 +369,320 @@ func filterAdjacencyForSPF(order []uint, adj map[uint][]Edge, policy RoutingPoli
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeInfo 保存边的端点信息,用于后续组装 PeerConfig
|
// filterAdjacencyForSymmetricLinks 仅保留“存在反向直连边”的邻接(用于可转发 SPF)。
|
||||||
type EdgeInfo struct {
|
func filterAdjacencyForSymmetricLinks(order []uint, adj map[uint][]Edge) map[uint][]Edge {
|
||||||
toEndpoint *models.Endpoint
|
ret := make(map[uint][]Edge, len(order))
|
||||||
|
edgeSet := make(map[[2]uint]struct{}, 64)
|
||||||
|
for from, edges := range adj {
|
||||||
|
for _, e := range edges {
|
||||||
|
edgeSet[[2]uint{from, e.to}] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for from, edges := range adj {
|
||||||
|
for _, e := range edges {
|
||||||
|
if _, ok := edgeSet[[2]uint{e.to, from}]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret[from] = append(ret[from], e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, id := range order {
|
||||||
|
if _, ok := ret[id]; !ok {
|
||||||
|
ret[id] = []Edge{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// runAllPairsDijkstra returns: map[src]map[nextHop]map[CIDR], map[src]map[nextHop]*EdgeInfo
|
type directedEdgeInfo struct {
|
||||||
func runAllPairsDijkstra(order []uint, adj map[uint][]Edge, idToPeer map[uint]*models.WireGuard, policy RoutingPolicy) (map[uint]map[uint]map[string]struct{}, map[uint]map[uint]*EdgeInfo) {
|
latency uint32
|
||||||
aggByNode := make(map[uint]map[uint]map[string]struct{}, len(order))
|
upMbps uint32
|
||||||
edgeInfoMap := make(map[uint]map[uint]*EdgeInfo, len(order)) // 保存 src -> nextHop 的边信息
|
toEndpoint *models.Endpoint
|
||||||
|
explicit bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type undirectedNeighbor struct {
|
||||||
|
to uint
|
||||||
|
weight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeAllowedIPs 是“最终下发路由”的核心:
|
||||||
|
// - 在 spfAdj 上构建“对称权重的无向图”
|
||||||
|
// - 对每个 src 做一次 Dijkstra,得到最短路树 prev
|
||||||
|
// - 同时生成 Out(dst prefixes) 与 In(src prefixes) 并合并到每条直连 peer 的 AllowedIPs
|
||||||
|
func computeAllowedIPs(
|
||||||
|
order []uint,
|
||||||
|
idToPeer map[uint]*models.WireGuard,
|
||||||
|
cidrByID map[uint]string,
|
||||||
|
spfAdj map[uint][]Edge,
|
||||||
|
fullAdj map[uint][]Edge, // 用于展示补齐 latency/up/endpoint
|
||||||
|
policy RoutingPolicy,
|
||||||
|
) (map[uint][]*pb.WireGuardPeerConfig, map[uint][]Edge, error) {
|
||||||
|
// 构建 directed edge info(用于 endpoint/展示),并构建 undirected graph(对称权重)
|
||||||
|
dInfo := make(map[[2]uint]*directedEdgeInfo, 128)
|
||||||
|
undir := make(map[uint][]undirectedNeighbor, len(order))
|
||||||
|
|
||||||
|
// 先把 spfAdj 的 directed info 记下来
|
||||||
|
for _, from := range order {
|
||||||
|
for _, e := range spfAdj[from] {
|
||||||
|
key := [2]uint{from, e.to}
|
||||||
|
dInfo[key] = &directedEdgeInfo{
|
||||||
|
latency: e.latency,
|
||||||
|
upMbps: e.upMbps,
|
||||||
|
toEndpoint: e.toEndpoint,
|
||||||
|
explicit: e.explicit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无向图:只添加“成对存在”的边,weight 用 max(w_uv, w_vu) 保证对称
|
||||||
|
added := make(map[[2]uint]struct{}, 128)
|
||||||
|
for _, u := range order {
|
||||||
|
for _, e := range spfAdj[u] {
|
||||||
|
v := e.to
|
||||||
|
if u == v {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 只处理一次 pair(u,v)
|
||||||
|
a, b := u, v
|
||||||
|
if a > b {
|
||||||
|
a, b = b, a
|
||||||
|
}
|
||||||
|
pair := [2]uint{a, b}
|
||||||
|
if _, ok := added[pair]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 需要双向边信息
|
||||||
|
uv, ok1 := dInfo[[2]uint{u, v}]
|
||||||
|
vu, ok2 := dInfo[[2]uint{v, u}]
|
||||||
|
if !ok1 || !ok2 || uv == nil || vu == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 用 policy.EdgeWeight 计算双向权重并取 max 做对称
|
||||||
|
wuv := policy.EdgeWeight(u, Edge{to: v, latency: uv.latency, upMbps: uv.upMbps, toEndpoint: uv.toEndpoint, explicit: uv.explicit}, idToPeer)
|
||||||
|
wvu := policy.EdgeWeight(v, Edge{to: u, latency: vu.latency, upMbps: vu.upMbps, toEndpoint: vu.toEndpoint, explicit: vu.explicit}, idToPeer)
|
||||||
|
w := math.Max(wuv, wvu)
|
||||||
|
undir[a] = append(undir[a], undirectedNeighbor{to: b, weight: w})
|
||||||
|
undir[b] = append(undir[b], undirectedNeighbor{to: a, weight: w})
|
||||||
|
added[pair] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 稳定排序
|
||||||
|
for _, u := range order {
|
||||||
|
neis := undir[u]
|
||||||
|
sort.SliceStable(neis, func(i, j int) bool { return neis[i].to < neis[j].to })
|
||||||
|
undir[u] = neis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out/ In 聚合:owner -> peer -> set[cidr]
|
||||||
|
allowed := make(map[uint]map[uint]map[string]struct{}, len(order))
|
||||||
|
|
||||||
for _, src := range order {
|
for _, src := range order {
|
||||||
dist, prev, visited := initSSSP(order)
|
dist := make(map[uint]float64, len(order))
|
||||||
|
prev := make(map[uint]uint, len(order)) // prev[dst] = predecessor of dst on path from src
|
||||||
|
visited := make(map[uint]bool, len(order))
|
||||||
|
for _, id := range order {
|
||||||
|
dist[id] = math.Inf(1)
|
||||||
|
}
|
||||||
dist[src] = 0
|
dist[src] = 0
|
||||||
|
|
||||||
|
// Dijkstra(O(n^2),节点数通常不大;同时保证确定性)
|
||||||
for {
|
for {
|
||||||
u, ok := pickNext(order, dist, visited)
|
u, ok := pickNext(order, dist, visited)
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
visited[u] = true
|
visited[u] = true
|
||||||
for _, e := range adj[u] {
|
for _, nb := range undir[u] {
|
||||||
w := policy.EdgeWeight(u, e, idToPeer)
|
v := nb.to
|
||||||
alt := dist[u] + w
|
if visited[v] {
|
||||||
if alt < dist[e.to] {
|
continue
|
||||||
dist[e.to] = alt
|
}
|
||||||
prev[e.to] = u
|
alt := dist[u] + nb.weight
|
||||||
|
if alt < dist[v] {
|
||||||
|
dist[v] = alt
|
||||||
|
prev[v] = u
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// tie-break:相同距离时,选择更小的 predecessor,确保稳定
|
||||||
|
if alt == dist[v] {
|
||||||
|
if cur, ok := prev[v]; !ok || u < cur {
|
||||||
|
prev[v] = u
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 累计 nextHop -> CIDR,并保存边信息
|
// 1) 出站目的集合:dstCIDR -> nextHop(src,dst)
|
||||||
for _, dst := range order {
|
for _, dst := range order {
|
||||||
if dst == src {
|
if dst == src {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := prev[dst]; !ok {
|
if _, ok := prev[dst]; !ok {
|
||||||
continue
|
continue // unreachable
|
||||||
}
|
}
|
||||||
next := findNextHop(src, dst, prev)
|
next := findNextHop(src, dst, prev)
|
||||||
if next == 0 {
|
if next == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dstPeer := idToPeer[dst]
|
cidr := cidrByID[dst]
|
||||||
allowed, err := dstPeer.AsBasePeerConfig(nil) // 这里只获取 CIDR,不需要指定 endpoint
|
if cidr == "" {
|
||||||
if err != nil || len(allowed.GetAllowedIps()) == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cidr := allowed.GetAllowedIps()[0]
|
ensureAllowedSet(allowed, src, next)[cidr] = struct{}{}
|
||||||
if _, ok := aggByNode[src]; !ok {
|
}
|
||||||
aggByNode[src] = make(map[uint]map[string]struct{})
|
|
||||||
}
|
|
||||||
if _, ok := aggByNode[src][next]; !ok {
|
|
||||||
aggByNode[src][next] = map[string]struct{}{}
|
|
||||||
}
|
|
||||||
aggByNode[src][next][cidr] = struct{}{}
|
|
||||||
|
|
||||||
// 保存从 src 到 next 的边信息(查找直接边)
|
// 2) 入站源集合:srcCIDR -> prevHop(src,dst) 归到 dst 节点的 peer(prevHop)
|
||||||
if _, ok := edgeInfoMap[src]; !ok {
|
srcCIDR := cidrByID[src]
|
||||||
edgeInfoMap[src] = make(map[uint]*EdgeInfo)
|
if srcCIDR != "" {
|
||||||
}
|
for _, dst := range order {
|
||||||
if _, ok := edgeInfoMap[src][next]; !ok {
|
if dst == src {
|
||||||
// 查找从 src 到 next 的边
|
continue
|
||||||
for _, e := range adj[src] {
|
|
||||||
if e.to == next {
|
|
||||||
edgeInfoMap[src][next] = &EdgeInfo{toEndpoint: e.toEndpoint}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
pred, ok := prev[dst]
|
||||||
|
if !ok || pred == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ensureAllowedSet(allowed, dst, pred)[srcCIDR] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return aggByNode, edgeInfoMap
|
|
||||||
|
// 构建 PeerConfigs,并做强校验(同一节点不允许 CIDR 分配到多个 peer)
|
||||||
|
result := make(map[uint][]*pb.WireGuardPeerConfig, len(order))
|
||||||
|
finalEdges := make(map[uint][]Edge, len(order))
|
||||||
|
|
||||||
|
for _, owner := range order {
|
||||||
|
peerToCIDRs := allowed[owner]
|
||||||
|
if len(peerToCIDRs) == 0 {
|
||||||
|
result[owner] = []*pb.WireGuardPeerConfig{}
|
||||||
|
finalEdges[owner] = []Edge{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]uint, 128)
|
||||||
|
peerIDs := lo.Keys(peerToCIDRs)
|
||||||
|
sort.Slice(peerIDs, func(i, j int) bool { return peerIDs[i] < peerIDs[j] })
|
||||||
|
|
||||||
|
pcs := make([]*pb.WireGuardPeerConfig, 0, len(peerIDs))
|
||||||
|
edges := make([]Edge, 0, len(peerIDs))
|
||||||
|
|
||||||
|
for _, peerID := range peerIDs {
|
||||||
|
cset := peerToCIDRs[peerID]
|
||||||
|
if len(cset) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remote := idToPeer[peerID]
|
||||||
|
if remote == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// endpoint:优先使用 spfAdj 的直连边的 toEndpoint(与实际更一致)
|
||||||
|
var specifiedEndpoint *models.Endpoint
|
||||||
|
if info := dInfo[[2]uint{owner, peerID}]; info != nil && info.toEndpoint != nil {
|
||||||
|
specifiedEndpoint = info.toEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
base, err := remote.AsBasePeerConfig(specifiedEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Join(errors.New("build peer base config failed"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cidrs := make([]string, 0, len(cset))
|
||||||
|
for c := range cset {
|
||||||
|
if prevOwner, ok := seen[c]; ok && prevOwner != peerID {
|
||||||
|
return nil, nil, fmt.Errorf("duplicate allowed ip on node %d: %s appears in peer %d and peer %d", owner, c, prevOwner, peerID)
|
||||||
|
}
|
||||||
|
seen[c] = peerID
|
||||||
|
cidrs = append(cidrs, c)
|
||||||
|
}
|
||||||
|
sort.Strings(cidrs)
|
||||||
|
base.AllowedIps = lo.Uniq(cidrs)
|
||||||
|
pcs = append(pcs, base)
|
||||||
|
|
||||||
|
// 用 fullAdj 补齐展示指标(latency/up/endpoint)
|
||||||
|
lat, up, ep, explicit := lookupEdgeForDisplay(fullAdj, owner, peerID)
|
||||||
|
edges = append(edges, Edge{
|
||||||
|
to: peerID,
|
||||||
|
latency: lat,
|
||||||
|
upMbps: up,
|
||||||
|
toEndpoint: ep,
|
||||||
|
routes: base.AllowedIps,
|
||||||
|
explicit: explicit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 client_id 稳定排序(保持原接口习惯)
|
||||||
|
sort.SliceStable(pcs, func(i, j int) bool { return pcs[i].GetClientId() < pcs[j].GetClientId() })
|
||||||
|
sort.SliceStable(edges, func(i, j int) bool { return edges[i].to < edges[j].to })
|
||||||
|
|
||||||
|
result[owner] = pcs
|
||||||
|
finalEdges[owner] = edges
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, finalEdges, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureAllowedSet(m map[uint]map[uint]map[string]struct{}, owner, peer uint) map[string]struct{} {
|
||||||
|
if _, ok := m[owner]; !ok {
|
||||||
|
m[owner] = make(map[uint]map[string]struct{}, 8)
|
||||||
|
}
|
||||||
|
if _, ok := m[owner][peer]; !ok {
|
||||||
|
m[owner][peer] = make(map[string]struct{}, 32)
|
||||||
|
}
|
||||||
|
return m[owner][peer]
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupEdgeForDisplay(fullAdj map[uint][]Edge, from, to uint) (latency uint32, up uint32, ep *models.Endpoint, explicit bool) {
|
||||||
|
edges := fullAdj[from]
|
||||||
|
for _, e := range edges {
|
||||||
|
if e.to == to {
|
||||||
|
return e.latency, e.upMbps, e.toEndpoint, e.explicit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickNext(order []uint, dist map[uint]float64, visited map[uint]bool) (uint, bool) {
|
||||||
|
best := uint(0)
|
||||||
|
bestVal := math.Inf(1)
|
||||||
|
found := false
|
||||||
|
for _, vid := range order {
|
||||||
|
if visited[vid] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dist[vid] < bestVal {
|
||||||
|
bestVal = dist[vid]
|
||||||
|
best = vid
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best, found
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNextHop 返回从 src 到 dst 的 nextHop(src 的直连邻居),依赖 prev[dst] = predecessor(dst)
|
||||||
|
func findNextHop(src, dst uint, prev map[uint]uint) uint {
|
||||||
|
next := dst
|
||||||
|
for {
|
||||||
|
p, ok := prev[next]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if p == src {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
next = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅用于测试/诊断:解析 /32 的 host ip(校验格式)
|
||||||
|
func parseHostFromCIDR(c string) (netip.Addr, bool) {
|
||||||
|
p, err := netip.ParsePrefix(c)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Addr{}, false
|
||||||
|
}
|
||||||
|
return p.Addr(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHandshakeAgeBetween 返回 a<->b 间 peer handshake 的“最大”年龄(只要任意一侧可观测到握手时间就生效)。
|
// getHandshakeAgeBetween 返回 a<->b 间 peer handshake 的“最大”年龄(只要任意一侧可观测到握手时间就生效)。
|
||||||
@@ -508,7 +705,7 @@ func getHandshakeAgeBetween(aWGID, bWGID uint, idToPeer map[uint]*models.WireGua
|
|||||||
return ageB, true
|
return ageB, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOneWayHandshakeAge 从 fromWGID 的 runtimeInfo 中,查找到 toWGID 对应 peer 的 last_handshake_time_sec/nsec,返回握手“距离现在”的时间差。
|
// getOneWayHandshakeAge 从 fromWGID 的 runtimeInfo 中查到 toWGID 对应 peer 的 last_handshake_time_sec/nsec,返回握手“距离现在”的时间差。
|
||||||
func getOneWayHandshakeAge(fromWGID, toWGID uint, idToPeer map[uint]*models.WireGuard, policy RoutingPolicy) (time.Duration, bool) {
|
func getOneWayHandshakeAge(fromWGID, toWGID uint, idToPeer map[uint]*models.WireGuard, policy RoutingPolicy) (time.Duration, bool) {
|
||||||
if policy.NetworkTopologyCache == nil {
|
if policy.NetworkTopologyCache == nil {
|
||||||
return 0, false
|
return 0, false
|
||||||
@@ -544,85 +741,3 @@ func getOneWayHandshakeAge(fromWGID, toWGID uint, idToPeer map[uint]*models.Wire
|
|||||||
}
|
}
|
||||||
return age, true
|
return age, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSSSP(order []uint) (map[uint]float64, map[uint]uint, map[uint]bool) {
|
|
||||||
dist := make(map[uint]float64, len(order))
|
|
||||||
prev := make(map[uint]uint, len(order))
|
|
||||||
visited := make(map[uint]bool, len(order))
|
|
||||||
for _, vid := range order {
|
|
||||||
dist[vid] = math.Inf(1)
|
|
||||||
}
|
|
||||||
return dist, prev, visited
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickNext(order []uint, dist map[uint]float64, visited map[uint]bool) (uint, bool) {
|
|
||||||
best := uint(0)
|
|
||||||
bestVal := math.Inf(1)
|
|
||||||
found := false
|
|
||||||
for _, vid := range order {
|
|
||||||
if visited[vid] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if dist[vid] < bestVal {
|
|
||||||
bestVal = dist[vid]
|
|
||||||
best = vid
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best, found
|
|
||||||
}
|
|
||||||
|
|
||||||
func findNextHop(src, dst uint, prev map[uint]uint) uint {
|
|
||||||
next := dst
|
|
||||||
for {
|
|
||||||
p, ok := prev[next]
|
|
||||||
if !ok {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if p == src {
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
next = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assemblePeerConfigs(order []uint, aggByNode map[uint]map[uint]map[string]struct{}, edgeInfoMap map[uint]map[uint]*EdgeInfo, idToPeer map[uint]*models.WireGuard) (map[uint][]*pb.WireGuardPeerConfig, error) {
|
|
||||||
result := make(map[uint][]*pb.WireGuardPeerConfig, len(order))
|
|
||||||
for src, nextMap := range aggByNode {
|
|
||||||
peersForSrc := make([]*pb.WireGuardPeerConfig, 0, len(nextMap))
|
|
||||||
for nextHop, cidrSet := range nextMap {
|
|
||||||
remote := idToPeer[nextHop]
|
|
||||||
|
|
||||||
// 获取从 src 到 nextHop 的边信息,确定使用哪个 endpoint
|
|
||||||
var specifiedEndpoint *models.Endpoint
|
|
||||||
if edgeInfo, ok := edgeInfoMap[src][nextHop]; ok && edgeInfo != nil && edgeInfo.toEndpoint != nil {
|
|
||||||
specifiedEndpoint = edgeInfo.toEndpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := remote.AsBasePeerConfig(specifiedEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Join(errors.New("build peer base config failed"), err)
|
|
||||||
}
|
|
||||||
cidrs := make([]string, 0, len(cidrSet))
|
|
||||||
for c := range cidrSet {
|
|
||||||
cidrs = append(cidrs, c)
|
|
||||||
}
|
|
||||||
sort.Strings(cidrs)
|
|
||||||
base.AllowedIps = lo.Uniq(cidrs)
|
|
||||||
peersForSrc = append(peersForSrc, base)
|
|
||||||
}
|
|
||||||
sort.SliceStable(peersForSrc, func(i, j int) bool {
|
|
||||||
return peersForSrc[i].GetClientId() < peersForSrc[j].GetClientId()
|
|
||||||
})
|
|
||||||
result[src] = peersForSrc
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fillIsolates(order []uint, result map[uint][]*pb.WireGuardPeerConfig) {
|
|
||||||
for _, id := range order {
|
|
||||||
if _, ok := result[id]; !ok {
|
|
||||||
result[id] = []*pb.WireGuardPeerConfig{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/VaalaCat/frp-panel/models"
|
"github.com/VaalaCat/frp-panel/models"
|
||||||
"github.com/VaalaCat/frp-panel/pb"
|
"github.com/VaalaCat/frp-panel/pb"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeTopologyCache struct {
|
type fakeTopologyCache struct {
|
||||||
@@ -90,93 +91,86 @@ func TestFilterAdjacencyForSPF(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunAllPairsDijkstra_PreferFreshHandshake(t *testing.T) {
|
func TestPlanAllowedIPs_PreferFreshHandshake(t *testing.T) {
|
||||||
// 1 -> 2 (stale handshake)
|
// 1 <-> 2:低延迟但握手过旧(应被惩罚)
|
||||||
// 1 -> 3 (fresh)
|
// 1 <-> 3 <-> 2:略高延迟但握手新(应被选为 1->2 的 nextHop=3)
|
||||||
// 3 -> 2 (fresh)
|
|
||||||
// 期望:从 1 到 2 的 nextHop 选择 3,而不是 2
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
priv1, _ := wgtypes.GeneratePrivateKey()
|
priv1, _ := wgtypes.GeneratePrivateKey()
|
||||||
priv2, _ := wgtypes.GeneratePrivateKey()
|
priv2, _ := wgtypes.GeneratePrivateKey()
|
||||||
priv3, _ := wgtypes.GeneratePrivateKey()
|
priv3, _ := wgtypes.GeneratePrivateKey()
|
||||||
|
|
||||||
p1 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
p1 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{ClientID: "c1", PrivateKey: priv1.String(), LocalAddress: "10.0.0.1/32"}}
|
||||||
ClientID: "c1",
|
|
||||||
PrivateKey: priv1.String(),
|
|
||||||
LocalAddress: "10.0.0.1/32",
|
|
||||||
}}
|
|
||||||
p1.ID = 1
|
p1.ID = 1
|
||||||
p2 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
p2 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{ClientID: "c2", PrivateKey: priv2.String(), LocalAddress: "10.0.0.2/32"}}
|
||||||
ClientID: "c2",
|
|
||||||
PrivateKey: priv2.String(),
|
|
||||||
LocalAddress: "10.0.0.2/32",
|
|
||||||
}}
|
|
||||||
p2.ID = 2
|
p2.ID = 2
|
||||||
p3 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
p3 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{ClientID: "c3", PrivateKey: priv3.String(), LocalAddress: "10.0.0.3/32"}}
|
||||||
ClientID: "c3",
|
|
||||||
PrivateKey: priv3.String(),
|
|
||||||
LocalAddress: "10.0.0.3/32",
|
|
||||||
}}
|
|
||||||
p3.ID = 3
|
p3.ID = 3
|
||||||
|
|
||||||
idToPeer := map[uint]*models.WireGuard{1: p1, 2: p2, 3: p3}
|
// 显式链路也要求至少一侧存在 endpoint(符合真实运行时:需要可连接入口)
|
||||||
order := []uint{1, 2, 3}
|
p1.AdvertisedEndpoints = []*models.Endpoint{{EndpointEntity: &models.EndpointEntity{Host: "redacted.example", Port: 61820, Type: "ws", WireGuardID: 1, ClientID: "c1"}}}
|
||||||
adj := map[uint][]Edge{
|
p2.AdvertisedEndpoints = []*models.Endpoint{{EndpointEntity: &models.EndpointEntity{Host: "redacted.example", Port: 61820, Type: "ws", WireGuardID: 2, ClientID: "c2"}}}
|
||||||
1: {
|
p3.AdvertisedEndpoints = []*models.Endpoint{{EndpointEntity: &models.EndpointEntity{Host: "redacted.example", Port: 61820, Type: "ws", WireGuardID: 3, ClientID: "c3"}}}
|
||||||
{to: 2, latency: 10, upMbps: 50, explicit: true},
|
|
||||||
{to: 3, latency: 5, upMbps: 50, explicit: true},
|
peers := []*models.WireGuard{p1, p2, p3}
|
||||||
},
|
link := func(from, to uint, latency uint32) *models.WireGuardLink {
|
||||||
3: {
|
return &models.WireGuardLink{WireGuardLinkEntity: &models.WireGuardLinkEntity{
|
||||||
{to: 2, latency: 5, upMbps: 50, explicit: true},
|
FromWireGuardID: from,
|
||||||
},
|
ToWireGuardID: to,
|
||||||
2: {},
|
UpBandwidthMbps: 50,
|
||||||
|
LatencyMs: latency,
|
||||||
|
Active: true,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
links := []*models.WireGuardLink{
|
||||||
|
link(1, 2, 5), link(2, 1, 5),
|
||||||
|
link(1, 3, 8), link(3, 1, 8),
|
||||||
|
link(3, 2, 8), link(2, 3, 8),
|
||||||
}
|
}
|
||||||
|
|
||||||
cache := &fakeTopologyCache{
|
cache := &fakeTopologyCache{
|
||||||
rt: map[uint]*pb.WGDeviceRuntimeInfo{
|
rt: map[uint]*pb.WGDeviceRuntimeInfo{
|
||||||
1: {
|
1: {Peers: []*pb.WGPeerRuntimeInfo{
|
||||||
Peers: []*pb.WGPeerRuntimeInfo{
|
{ClientId: "c2", LastHandshakeTimeSec: uint64(now - 3600)}, // stale
|
||||||
{ClientId: "c2", LastHandshakeTimeSec: uint64(now - 3600)}, // stale
|
{ClientId: "c3", LastHandshakeTimeSec: uint64(now)}, // fresh
|
||||||
{ClientId: "c3", LastHandshakeTimeSec: uint64(now)}, // fresh
|
}},
|
||||||
},
|
2: {Peers: []*pb.WGPeerRuntimeInfo{
|
||||||
},
|
{ClientId: "c1", LastHandshakeTimeSec: uint64(now - 3600)}, // stale (对称)
|
||||||
3: {
|
{ClientId: "c3", LastHandshakeTimeSec: uint64(now)}, // fresh
|
||||||
Peers: []*pb.WGPeerRuntimeInfo{
|
}},
|
||||||
{ClientId: "c2", LastHandshakeTimeSec: uint64(now)}, // fresh
|
3: {Peers: []*pb.WGPeerRuntimeInfo{
|
||||||
},
|
{ClientId: "c1", LastHandshakeTimeSec: uint64(now)},
|
||||||
},
|
{ClientId: "c2", LastHandshakeTimeSec: uint64(now)},
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
policy := RoutingPolicy{
|
policy := DefaultRoutingPolicy(NewACL(), cache, nil)
|
||||||
LatencyWeight: 1,
|
policy.HandshakeStaleThreshold = 1 * time.Second
|
||||||
InverseBandwidthWeight: 0,
|
policy.HandshakeStalePenalty = 1000
|
||||||
HopWeight: 0,
|
policy.InverseBandwidthWeight = 0
|
||||||
HandshakeStaleThreshold: 1 * time.Second,
|
policy.HopWeight = 0
|
||||||
HandshakeStalePenalty: 100,
|
policy.LatencyLogScale = 0
|
||||||
NetworkTopologyCache: cache,
|
|
||||||
|
peerCfgs, _, err := PlanAllowedIPs(peers, links, policy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PlanAllowedIPs err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
aggByNode, _ := runAllPairsDijkstra(order, adj, idToPeer, policy)
|
// 对 node1:10.0.0.2/32 应走 peer(3) 而不是 peer(2)
|
||||||
if aggByNode[1] == nil {
|
wantDst := "10.0.0.2/32"
|
||||||
t.Fatalf("aggByNode[1] should not be nil")
|
var gotPeer uint32
|
||||||
|
for _, pc := range peerCfgs[1] {
|
||||||
|
if pc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lo.Contains(pc.GetAllowedIps(), wantDst) {
|
||||||
|
gotPeer = pc.GetId()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// dst=2 的 CIDR 应该被聚合到 nextHop=3 下(而不是 nextHop=2)
|
if gotPeer != 3 {
|
||||||
if _, ok := aggByNode[1][3]; !ok {
|
t.Fatalf("want node1 route %s via peer 3, got peer %d", wantDst, gotPeer)
|
||||||
t.Fatalf("want nextHop=3 for src=1, got keys=%v", keysUint(aggByNode[1]))
|
|
||||||
}
|
}
|
||||||
if _, ok := aggByNode[1][2]; ok {
|
|
||||||
t.Fatalf("did not expect nextHop=2 for src=1 when handshake is stale")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func keysUint(m map[uint]map[string]struct{}) []uint {
|
|
||||||
ret := make([]uint, 0, len(m))
|
|
||||||
for k := range m {
|
|
||||||
ret = append(ret, k)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSymmetrizeAdjacencyForPeers_FillReverseEdge(t *testing.T) {
|
func TestSymmetrizeAdjacencyForPeers_FillReverseEdge(t *testing.T) {
|
||||||
@@ -199,88 +193,205 @@ func TestFilterAdjacencyForSymmetricLinks_DropOneWay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsureRoutingPeerSymmetry_AddReversePeer(t *testing.T) {
|
func TestEnsureRoutingPeerSymmetry_AddReversePeer(t *testing.T) {
|
||||||
// 构造一个“1 直连 2,但 2 到 1 会更偏好走 3”的场景:
|
t.Skip("routing planner rewritten: inbound-source-set generation replaces old symmetry patching")
|
||||||
// 1->2 成为承载路由的 nextHop,但 2 的路由结果中可能不包含 peer(1),需要对称补齐。
|
}
|
||||||
now := time.Now().Unix()
|
|
||||||
|
|
||||||
priv1, _ := wgtypes.GeneratePrivateKey()
|
func TestPlanAllowedIPs_Regression_NoDuplicateAllowedIPs_And_TransitSourceValidation(t *testing.T) {
|
||||||
priv2, _ := wgtypes.GeneratePrivateKey()
|
// 复现 & 防回归:
|
||||||
priv3, _ := wgtypes.GeneratePrivateKey()
|
// 1) 同一节点的 AllowedIPs 不允许在多个 peer 间重复(例如 10.10.0.4/32 只能分配给一个 nextHop)
|
||||||
|
// 2) 多跳转发时,入站 source validation 需要允许“原始源地址”:
|
||||||
|
// 构造 21(10.10.0.8) -> 16(10.10.0.2) 走 24 中转,
|
||||||
|
// 期望 16 的 peer(24) AllowedIPs 包含 10.10.0.8/32(否则 16 会丢弃来自 24 的转发包)。
|
||||||
|
|
||||||
p1 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
type node struct {
|
||||||
ClientID: "c1",
|
id uint
|
||||||
PrivateKey: priv1.String(),
|
cid string
|
||||||
LocalAddress: "10.0.0.1/32",
|
addr string
|
||||||
}}
|
tags []string
|
||||||
p1.ID = 1
|
hasEP bool
|
||||||
p2 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
|
||||||
ClientID: "c2",
|
|
||||||
PrivateKey: priv2.String(),
|
|
||||||
LocalAddress: "10.0.0.2/32",
|
|
||||||
}}
|
|
||||||
p2.ID = 2
|
|
||||||
p3 := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
|
||||||
ClientID: "c3",
|
|
||||||
PrivateKey: priv3.String(),
|
|
||||||
LocalAddress: "10.0.0.3/32",
|
|
||||||
}}
|
|
||||||
p3.ID = 3
|
|
||||||
|
|
||||||
idToPeer := map[uint]*models.WireGuard{1: p1, 2: p2, 3: p3}
|
|
||||||
order := []uint{1, 2, 3}
|
|
||||||
|
|
||||||
// 全双向连通,但设置权重让 2->1 更偏好 2->3->1
|
|
||||||
adj := map[uint][]Edge{
|
|
||||||
1: {
|
|
||||||
{to: 2, latency: 1, upMbps: 50, explicit: true},
|
|
||||||
{to: 3, latency: 100, upMbps: 50, explicit: true},
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
{to: 1, latency: 100, upMbps: 50, explicit: true},
|
|
||||||
{to: 3, latency: 1, upMbps: 50, explicit: true},
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
{to: 1, latency: 1, upMbps: 50, explicit: true},
|
|
||||||
{to: 2, latency: 100, upMbps: 50, explicit: true},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cache := &fakeTopologyCache{
|
nodes := []node{
|
||||||
rt: map[uint]*pb.WGDeviceRuntimeInfo{
|
{id: 4, cid: "c4", addr: "10.10.0.4/24", tags: []string{"cn", "bj"}, hasEP: true},
|
||||||
1: {Peers: []*pb.WGPeerRuntimeInfo{{ClientId: "c2", LastHandshakeTimeSec: uint64(now)}}},
|
{id: 11, cid: "c11", addr: "10.10.0.1/24", tags: []string{"cn", "wh"}, hasEP: false},
|
||||||
2: {Peers: []*pb.WGPeerRuntimeInfo{{ClientId: "c1", LastHandshakeTimeSec: uint64(now)}}},
|
{id: 16, cid: "c16", addr: "10.10.0.2/24", tags: []string{"cn", "bj", "ali"}, hasEP: true},
|
||||||
},
|
{id: 17, cid: "c17", addr: "10.10.0.3/24", tags: []string{"cn", "wh"}, hasEP: false},
|
||||||
|
{id: 18, cid: "c18", addr: "10.10.0.6/24", tags: []string{"us"}, hasEP: true},
|
||||||
|
{id: 20, cid: "c20", addr: "10.10.0.7/24", tags: []string{"us"}, hasEP: false},
|
||||||
|
{id: 21, cid: "c21", addr: "10.10.0.8/24", tags: []string{"cn", "nc"}, hasEP: false},
|
||||||
|
{id: 22, cid: "c22", addr: "10.10.0.9/24", tags: []string{"cn", "nc"}, hasEP: false},
|
||||||
|
{id: 24, cid: "c24", addr: "10.10.0.5/24", tags: []string{"cn", "nc"}, hasEP: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
policy := RoutingPolicy{
|
makePeer := func(n node) *models.WireGuard {
|
||||||
LatencyWeight: 1,
|
priv, _ := wgtypes.GeneratePrivateKey()
|
||||||
InverseBandwidthWeight: 0,
|
wg := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
||||||
HopWeight: 0,
|
ClientID: n.cid,
|
||||||
HandshakeStaleThreshold: 1 * time.Hour,
|
PrivateKey: priv.String(),
|
||||||
HandshakeStalePenalty: 0,
|
LocalAddress: n.addr,
|
||||||
NetworkTopologyCache: cache,
|
Tags: n.tags,
|
||||||
|
}}
|
||||||
|
wg.ID = n.id
|
||||||
|
if n.hasEP {
|
||||||
|
wg.AdvertisedEndpoints = []*models.Endpoint{
|
||||||
|
{EndpointEntity: &models.EndpointEntity{
|
||||||
|
Host: "redacted.example",
|
||||||
|
Port: 61820,
|
||||||
|
Type: "ws",
|
||||||
|
WireGuardID: n.id,
|
||||||
|
ClientID: n.cid,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wg
|
||||||
}
|
}
|
||||||
|
|
||||||
aggByNode, edgeInfo := runAllPairsDijkstra(order, adj, idToPeer, policy)
|
peers := lo.Map(nodes, func(n node, _ int) *models.WireGuard { return makePeer(n) })
|
||||||
peersMap, err := assemblePeerConfigs(order, aggByNode, edgeInfo, idToPeer)
|
|
||||||
|
// 构造 ACL(与用户提供一致:只验证 tag 匹配逻辑正确,不涉及公网信息)
|
||||||
|
acl := NewACL().LoadFromPB(&pb.AclConfig{Acls: []*pb.AclRuleConfig{
|
||||||
|
{Action: "allow", Src: []string{"bj", "wh"}, Dst: []string{"bj", "wh"}},
|
||||||
|
{Action: "allow", Src: []string{"nc", "wh"}, Dst: []string{"nc", "wh"}},
|
||||||
|
{Action: "allow", Src: []string{"nc", "ali"}, Dst: []string{"nc", "ali"}},
|
||||||
|
{Action: "allow", Src: []string{"wh", "ali"}, Dst: []string{"wh", "ali"}},
|
||||||
|
{Action: "allow", Src: []string{"us"}, Dst: []string{"us"}},
|
||||||
|
}})
|
||||||
|
|
||||||
|
// 只需要 latency cache 为推断边提供“探测存在性”,这里直接手动构造显式 links,更可控
|
||||||
|
// 关键:让 21->16 走 24 中转(21-24-16 低延迟,21-16 高延迟)
|
||||||
|
link := func(from, to uint, latency uint32) *models.WireGuardLink {
|
||||||
|
return &models.WireGuardLink{WireGuardLinkEntity: &models.WireGuardLinkEntity{
|
||||||
|
FromWireGuardID: from,
|
||||||
|
ToWireGuardID: to,
|
||||||
|
UpBandwidthMbps: 50,
|
||||||
|
LatencyMs: latency,
|
||||||
|
Active: true,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
links := []*models.WireGuardLink{
|
||||||
|
link(21, 24, 10), link(24, 21, 10),
|
||||||
|
link(24, 16, 10), link(16, 24, 10),
|
||||||
|
link(21, 16, 200), link(16, 21, 200),
|
||||||
|
|
||||||
|
// 再补一些连通边,确保能算出包含 4 的路由
|
||||||
|
link(11, 16, 30), link(16, 11, 30),
|
||||||
|
link(16, 4, 5), link(4, 16, 5),
|
||||||
|
link(11, 4, 50), link(4, 11, 50),
|
||||||
|
}
|
||||||
|
|
||||||
|
policy := DefaultRoutingPolicy(acl, &fakeTopologyCache{lat: map[[2]uint]uint32{}}, nil)
|
||||||
|
policy.HandshakeStalePenalty = 0
|
||||||
|
policy.HandshakeStaleThreshold = 0
|
||||||
|
policy.InverseBandwidthWeight = 0
|
||||||
|
policy.HopWeight = 0
|
||||||
|
policy.LatencyLogScale = 0
|
||||||
|
|
||||||
|
peerCfgs, _, err := PlanAllowedIPs(peers, links, policy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("assemblePeerConfigs err: %v", err)
|
t.Fatalf("PlanAllowedIPs err: %v", err)
|
||||||
}
|
}
|
||||||
fillIsolates(order, peersMap)
|
|
||||||
|
|
||||||
// 预期:在没有对称补齐前,2 可能不会包含 peer(1)
|
// 1) 断言:每个节点的 AllowedIPs 在不同 peer 间不重复
|
||||||
_ = ensureRoutingPeerSymmetry(order, peersMap, idToPeer)
|
for owner, pcs := range peerCfgs {
|
||||||
|
seen := map[string]uint32{}
|
||||||
found := false
|
for _, pc := range pcs {
|
||||||
for _, pc := range peersMap[2] {
|
if pc == nil {
|
||||||
if pc != nil && pc.GetId() == 1 {
|
continue
|
||||||
found = true
|
}
|
||||||
if len(pc.GetAllowedIps()) == 0 || pc.GetAllowedIps()[0] != "10.0.0.1/32" {
|
for _, cidr := range pc.GetAllowedIps() {
|
||||||
t.Fatalf("peer(1) on node2 should include 10.0.0.1/32, got=%v", pc.GetAllowedIps())
|
if prev, ok := seen[cidr]; ok && prev != pc.GetId() {
|
||||||
|
t.Fatalf("node %d has duplicate cidr %s on peer %d and peer %d", owner, cidr, prev, pc.GetId())
|
||||||
|
}
|
||||||
|
seen[cidr] = pc.GetId()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) 断言:16 的 peer(24) 必须包含 10.10.0.8/32(21 的 /32),用于入站 source validation
|
||||||
|
wantSrc := "10.10.0.8/32"
|
||||||
|
found := false
|
||||||
|
for _, pc := range peerCfgs[16] {
|
||||||
|
if pc == nil || pc.GetId() != 24 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lo.Contains(pc.GetAllowedIps(), wantSrc) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if !found {
|
if !found {
|
||||||
t.Fatalf("node2 should contain peer(1) after ensureRoutingPeerSymmetry")
|
t.Fatalf("node 16 peer(24) should contain %s for transit source validation", wantSrc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 断言:11 节点的 10.10.0.4/32 不能同时出现在多个 peer
|
||||||
|
wantC4 := "10.10.0.4/32"
|
||||||
|
var peersWithC4 []uint32
|
||||||
|
for _, pc := range peerCfgs[11] {
|
||||||
|
if pc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lo.Contains(pc.GetAllowedIps(), wantC4) {
|
||||||
|
peersWithC4 = append(peersWithC4, pc.GetId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(peersWithC4) != 1 {
|
||||||
|
t.Fatalf("node 11 should have exactly one peer carrying %s, got peers=%v", wantC4, peersWithC4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildAdjacency_InferredEdgesAreBidirectionalWhenACLAllows(t *testing.T) {
|
||||||
|
// 回归:推断边必须支持 to(with endpoint) -> from(no endpoint) 的反向补齐,
|
||||||
|
// 否则 filterAdjacencyForSymmetricLinks 会把所有 “no-endpoint 节点” 剔除,导致 SPF 结果为空。
|
||||||
|
|
||||||
|
privA, _ := wgtypes.GeneratePrivateKey()
|
||||||
|
privB, _ := wgtypes.GeneratePrivateKey()
|
||||||
|
|
||||||
|
a := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
||||||
|
ClientID: "ca",
|
||||||
|
PrivateKey: privA.String(),
|
||||||
|
LocalAddress: "10.0.0.1/24",
|
||||||
|
Tags: []string{"t1"},
|
||||||
|
}}
|
||||||
|
a.ID = 1 // no endpoint
|
||||||
|
|
||||||
|
b := &models.WireGuard{WireGuardEntity: &models.WireGuardEntity{
|
||||||
|
ClientID: "cb",
|
||||||
|
PrivateKey: privB.String(),
|
||||||
|
LocalAddress: "10.0.0.2/24",
|
||||||
|
Tags: []string{"t1"},
|
||||||
|
}}
|
||||||
|
b.ID = 2
|
||||||
|
b.AdvertisedEndpoints = []*models.Endpoint{{EndpointEntity: &models.EndpointEntity{
|
||||||
|
Host: "redacted.example",
|
||||||
|
Port: 61820,
|
||||||
|
Type: "ws",
|
||||||
|
WireGuardID: 2,
|
||||||
|
ClientID: "cb",
|
||||||
|
}}}
|
||||||
|
|
||||||
|
idToPeer, order := buildNodeIndexSorted([]*models.WireGuard{a, b})
|
||||||
|
acl := NewACL().LoadFromPB(&pb.AclConfig{Acls: []*pb.AclRuleConfig{
|
||||||
|
{Action: "allow", Src: []string{"t1"}, Dst: []string{"t1"}},
|
||||||
|
}})
|
||||||
|
policy := DefaultRoutingPolicy(acl, &fakeTopologyCache{lat: map[[2]uint]uint32{
|
||||||
|
{1, 2}: 10,
|
||||||
|
{2, 1}: 10,
|
||||||
|
}}, nil)
|
||||||
|
|
||||||
|
adj := buildAdjacency(order, idToPeer, nil, policy)
|
||||||
|
// 期望:1->2 与 2->1 都存在(推断边双向)
|
||||||
|
has12 := false
|
||||||
|
for _, e := range adj[1] {
|
||||||
|
if e.to == 2 {
|
||||||
|
has12 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
has21 := false
|
||||||
|
for _, e := range adj[2] {
|
||||||
|
if e.to == 1 {
|
||||||
|
has21 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !has12 || !has21 {
|
||||||
|
t.Fatalf("want inferred edges 1->2 and 2->1, got has12=%v has21=%v adj=%#v", has12, has21, adj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user