mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-11 09:21:49 +01:00
2475473227
All routes are now installed in a custom netbird routing table. Management and wireguard traffic is now marked with a custom fwmark. When the mark is present the traffic is routed via the main routing table, bypassing the VPN. When the mark is absent the traffic is routed via the netbird routing table, if: - there's no match in the main routing table - it would match the default route in the routing table IPv6 traffic is blocked when a default route IPv4 route is configured to avoid leakage.
328 lines
9.1 KiB
Go
328 lines
9.1 KiB
Go
package routemanager
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/netip"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/iface"
|
|
"github.com/netbirdio/netbird/route"
|
|
)
|
|
|
|
const minRangeBits = 7
|
|
|
|
type routerPeerStatus struct {
|
|
connected bool
|
|
relayed bool
|
|
direct bool
|
|
}
|
|
|
|
type routesUpdate struct {
|
|
updateSerial uint64
|
|
routes []*route.Route
|
|
}
|
|
|
|
type clientNetwork struct {
|
|
ctx context.Context
|
|
stop context.CancelFunc
|
|
statusRecorder *peer.Status
|
|
wgInterface *iface.WGIface
|
|
routes map[string]*route.Route
|
|
routeUpdate chan routesUpdate
|
|
peerStateUpdate chan struct{}
|
|
routePeersNotifiers map[string]chan struct{}
|
|
chosenRoute *route.Route
|
|
network netip.Prefix
|
|
updateSerial uint64
|
|
}
|
|
|
|
func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, statusRecorder *peer.Status, network netip.Prefix) *clientNetwork {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
client := &clientNetwork{
|
|
ctx: ctx,
|
|
stop: cancel,
|
|
statusRecorder: statusRecorder,
|
|
wgInterface: wgInterface,
|
|
routes: make(map[string]*route.Route),
|
|
routePeersNotifiers: make(map[string]chan struct{}),
|
|
routeUpdate: make(chan routesUpdate),
|
|
peerStateUpdate: make(chan struct{}),
|
|
network: network,
|
|
}
|
|
return client
|
|
}
|
|
|
|
func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
|
|
routePeerStatuses := make(map[string]routerPeerStatus)
|
|
for _, r := range c.routes {
|
|
peerStatus, err := c.statusRecorder.GetPeer(r.Peer)
|
|
if err != nil {
|
|
log.Debugf("couldn't fetch peer state: %v", err)
|
|
continue
|
|
}
|
|
routePeerStatuses[r.ID] = routerPeerStatus{
|
|
connected: peerStatus.ConnStatus == peer.StatusConnected,
|
|
relayed: peerStatus.Relayed,
|
|
direct: peerStatus.Direct,
|
|
}
|
|
}
|
|
return routePeerStatuses
|
|
}
|
|
|
|
// getBestRouteFromStatuses determines the most optimal route from the available routes
|
|
// within a clientNetwork, taking into account peer connection status, route metrics, and
|
|
// preference for non-relayed and direct connections.
|
|
//
|
|
// It follows these prioritization rules:
|
|
// * Connected peers: Only routes with connected peers are considered.
|
|
// * Metric: Routes with lower metrics (better) are prioritized.
|
|
// * Non-relayed: Routes without relays are preferred.
|
|
// * Direct connections: Routes with direct peer connections are favored.
|
|
// * Stability: In case of equal scores, the currently active route (if any) is maintained.
|
|
//
|
|
// It returns the ID of the selected optimal route.
|
|
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]routerPeerStatus) string {
|
|
chosen := ""
|
|
chosenScore := 0
|
|
|
|
currID := ""
|
|
if c.chosenRoute != nil {
|
|
currID = c.chosenRoute.ID
|
|
}
|
|
|
|
for _, r := range c.routes {
|
|
tempScore := 0
|
|
peerStatus, found := routePeerStatuses[r.ID]
|
|
if !found || !peerStatus.connected {
|
|
continue
|
|
}
|
|
|
|
if r.Metric < route.MaxMetric {
|
|
metricDiff := route.MaxMetric - r.Metric
|
|
tempScore = metricDiff * 10
|
|
}
|
|
|
|
if !peerStatus.relayed {
|
|
tempScore++
|
|
}
|
|
|
|
if peerStatus.direct {
|
|
tempScore++
|
|
}
|
|
|
|
if tempScore > chosenScore || (tempScore == chosenScore && r.ID == currID) {
|
|
chosen = r.ID
|
|
chosenScore = tempScore
|
|
}
|
|
|
|
if chosen == "" && currID == "" {
|
|
chosen = r.ID
|
|
chosenScore = tempScore
|
|
}
|
|
}
|
|
|
|
if chosen == "" {
|
|
var peers []string
|
|
for _, r := range c.routes {
|
|
peers = append(peers, r.Peer)
|
|
}
|
|
|
|
log.Warnf("the network %s has not been assigned a routing peer as no peers from the list %s are currently connected", c.network, peers)
|
|
|
|
} else if chosen != currID {
|
|
log.Infof("new chosen route is %s with peer %s with score %d for network %s", chosen, c.routes[chosen].Peer, chosenScore, c.network)
|
|
}
|
|
|
|
return chosen
|
|
}
|
|
|
|
func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-closer:
|
|
return
|
|
case <-c.statusRecorder.GetPeerStateChangeNotifier(peerKey):
|
|
state, err := c.statusRecorder.GetPeer(peerKey)
|
|
if err != nil || state.ConnStatus == peer.StatusConnecting {
|
|
continue
|
|
}
|
|
peerStateUpdate <- struct{}{}
|
|
log.Debugf("triggered route state update for Peer %s, state: %s", peerKey, state.ConnStatus)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *clientNetwork) startPeersStatusChangeWatcher() {
|
|
for _, r := range c.routes {
|
|
_, found := c.routePeersNotifiers[r.Peer]
|
|
if !found {
|
|
c.routePeersNotifiers[r.Peer] = make(chan struct{})
|
|
go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, c.routePeersNotifiers[r.Peer])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
|
|
state, err := c.statusRecorder.GetPeer(peerKey)
|
|
if err != nil {
|
|
return fmt.Errorf("get peer state: %v", err)
|
|
}
|
|
|
|
delete(state.Routes, c.network.String())
|
|
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
|
|
log.Warnf("Failed to update peer state: %v", err)
|
|
}
|
|
|
|
if state.ConnStatus != peer.StatusConnected {
|
|
return nil
|
|
}
|
|
|
|
err = c.wgInterface.RemoveAllowedIP(peerKey, c.network.String())
|
|
if err != nil {
|
|
return fmt.Errorf("remove allowed IP %s removed for peer %s, err: %v",
|
|
c.network, c.chosenRoute.Peer, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
|
|
if c.chosenRoute != nil {
|
|
if err := removeFromRouteTableIfNonSystem(c.network, c.wgInterface.Address().IP.String(), c.wgInterface.Name()); err != nil {
|
|
return fmt.Errorf("remove route %s from system, err: %v", c.network, err)
|
|
}
|
|
|
|
if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil {
|
|
return fmt.Errorf("remove route: %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
|
routerPeerStatuses := c.getRouterPeerStatuses()
|
|
|
|
chosen := c.getBestRouteFromStatuses(routerPeerStatuses)
|
|
|
|
// If no route is chosen, remove the route from the peer and system
|
|
if chosen == "" {
|
|
if err := c.removeRouteFromPeerAndSystem(); err != nil {
|
|
return fmt.Errorf("remove route from peer and system: %v", err)
|
|
}
|
|
|
|
c.chosenRoute = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
// If the chosen route is the same as the current route, do nothing
|
|
if c.chosenRoute != nil && c.chosenRoute.ID == chosen {
|
|
if c.chosenRoute.IsEqual(c.routes[chosen]) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if c.chosenRoute != nil {
|
|
// If a previous route exists, remove it from the peer
|
|
if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil {
|
|
return fmt.Errorf("remove route from peer: %v", err)
|
|
}
|
|
} else {
|
|
// otherwise add the route to the system
|
|
if err := addToRouteTableIfNoExists(c.network, c.wgInterface.Address().IP.String(), c.wgInterface.Name()); err != nil {
|
|
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
|
|
c.network.String(), c.wgInterface.Address().IP.String(), err)
|
|
}
|
|
}
|
|
|
|
c.chosenRoute = c.routes[chosen]
|
|
|
|
state, err := c.statusRecorder.GetPeer(c.chosenRoute.Peer)
|
|
if err != nil {
|
|
log.Errorf("Failed to get peer state: %v", err)
|
|
} else {
|
|
if state.Routes == nil {
|
|
state.Routes = map[string]struct{}{}
|
|
}
|
|
state.Routes[c.network.String()] = struct{}{}
|
|
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
|
|
log.Warnf("Failed to update peer state: %v", err)
|
|
}
|
|
}
|
|
|
|
if err := c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String()); err != nil {
|
|
log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v",
|
|
c.network, c.chosenRoute.Peer, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
|
|
go func() {
|
|
c.routeUpdate <- update
|
|
}()
|
|
}
|
|
|
|
func (c *clientNetwork) handleUpdate(update routesUpdate) {
|
|
updateMap := make(map[string]*route.Route)
|
|
|
|
for _, r := range update.routes {
|
|
updateMap[r.ID] = r
|
|
}
|
|
|
|
for id, r := range c.routes {
|
|
_, found := updateMap[id]
|
|
if !found {
|
|
close(c.routePeersNotifiers[r.Peer])
|
|
delete(c.routePeersNotifiers, r.Peer)
|
|
}
|
|
}
|
|
|
|
c.routes = updateMap
|
|
}
|
|
|
|
// peersStateAndUpdateWatcher is the main point of reacting on client network routing events.
|
|
// All the processing related to the client network should be done here. Thread-safe.
|
|
func (c *clientNetwork) peersStateAndUpdateWatcher() {
|
|
for {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
log.Debugf("stopping watcher for network %s", c.network)
|
|
err := c.removeRouteFromPeerAndSystem()
|
|
if err != nil {
|
|
log.Errorf("Couldn't remove route from peer and system for network %s: %v", c.network, err)
|
|
}
|
|
return
|
|
case <-c.peerStateUpdate:
|
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
|
if err != nil {
|
|
log.Errorf("Couldn't recalculate route and update peer and system: %v", err)
|
|
}
|
|
case update := <-c.routeUpdate:
|
|
if update.updateSerial < c.updateSerial {
|
|
log.Warnf("Received a routes update with smaller serial number, ignoring it")
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Received a new client network route update for %s", c.network)
|
|
|
|
c.handleUpdate(update)
|
|
|
|
c.updateSerial = update.updateSerial
|
|
|
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
|
if err != nil {
|
|
log.Errorf("Couldn't recalculate route and update peer and system for network %s: %v", c.network, err)
|
|
}
|
|
|
|
c.startPeersStatusChangeWatcher()
|
|
}
|
|
}
|
|
}
|