|
|
@@ -117,11 +117,23 @@ func (m *RoutingManager) applyAll() {
|
|
|
|
|
|
// ==================== Routes (/ip route) ====================
|
|
|
|
|
|
-func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
- m.mu.Lock()
|
|
|
- defer m.mu.Unlock()
|
|
|
+func isDynamicProtocol(proto netlink.RouteProtocol) bool {
|
|
|
+ switch int(proto) {
|
|
|
+ case 11, // RTPROT_ZEBRA
|
|
|
+ 16, // RTPROT_DHCP
|
|
|
+ 42, // RTPROT_BABEL
|
|
|
+ 186, // RTPROT_BGP
|
|
|
+ 187, // RTPROT_ISIS
|
|
|
+ 188, // RTPROT_OSPF
|
|
|
+ 189, // RTPROT_RIP
|
|
|
+ 192: // RTPROT_EIGRP
|
|
|
+ return true
|
|
|
+ default:
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // 1. Get live routes from netlink across all tables
|
|
|
+func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
|
|
|
tableNames := m.getTableMap()
|
|
|
links, _ := netlink.LinkList()
|
|
|
linkMap := make(map[int]string)
|
|
|
@@ -150,10 +162,19 @@ func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
}
|
|
|
|
|
|
ifName := linkMap[r.LinkIndex]
|
|
|
- isDirect := r.Scope == netlink.SCOPE_LINK
|
|
|
+ isDirect := r.Scope == netlink.SCOPE_LINK || r.Protocol == netlink.RouteProtocol(2)
|
|
|
+ isDyn := isDynamicProtocol(r.Protocol)
|
|
|
+ isStatic := !isDirect && !isDyn
|
|
|
+
|
|
|
+ cleanDst := strings.ReplaceAll(dst, "/", "_")
|
|
|
+ cleanGw := gw
|
|
|
+ if cleanGw == "" {
|
|
|
+ cleanGw = ifName
|
|
|
+ }
|
|
|
+ routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
|
|
|
|
|
|
liveRoutes = append(liveRoutes, RouteItem{
|
|
|
- ID: fmt.Sprintf("kr-%d-%s-%s", r.Table, dst, gw),
|
|
|
+ ID: routeID,
|
|
|
DstAddress: dst,
|
|
|
Gateway: gw,
|
|
|
Interface: ifName,
|
|
|
@@ -162,21 +183,33 @@ func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
TableID: r.Table,
|
|
|
Active: true,
|
|
|
Connect: isDirect,
|
|
|
- Dynamic: !isDirect && (r.Protocol == netlink.RouteProtocol(2) || r.Protocol == netlink.RouteProtocol(3)),
|
|
|
- Static: !isDirect && r.Protocol != netlink.RouteProtocol(2) && r.Protocol != netlink.RouteProtocol(3),
|
|
|
+ Dynamic: isDyn,
|
|
|
+ Static: isStatic,
|
|
|
Disabled: false,
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
+ return liveRoutes
|
|
|
+}
|
|
|
|
|
|
- // Merge static routes that might be disabled or pending
|
|
|
+func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
+ m.mu.Lock()
|
|
|
+ defer m.mu.Unlock()
|
|
|
+
|
|
|
+ liveRoutes := m.listLiveRoutesLocked()
|
|
|
+
|
|
|
+ // Merge static routes that might be disabled or have custom metadata
|
|
|
for _, sr := range m.staticRoutes {
|
|
|
found := false
|
|
|
for i, lr := range liveRoutes {
|
|
|
- if lr.DstAddress == sr.DstAddress && lr.RoutingTable == sr.RoutingTable && (lr.Gateway == sr.Gateway || sr.Gateway == "") {
|
|
|
+ if lr.DstAddress == sr.DstAddress && lr.RoutingTable == sr.RoutingTable && (sr.Gateway == "" || lr.Gateway == sr.Gateway) {
|
|
|
liveRoutes[i].Comment = sr.Comment
|
|
|
liveRoutes[i].Static = true
|
|
|
+ liveRoutes[i].Dynamic = false
|
|
|
liveRoutes[i].ID = sr.ID
|
|
|
+ if sr.Distance > 0 && liveRoutes[i].Distance == 0 {
|
|
|
+ liveRoutes[i].Distance = sr.Distance
|
|
|
+ }
|
|
|
found = true
|
|
|
break
|
|
|
}
|
|
|
@@ -186,6 +219,7 @@ func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
srCopy := sr
|
|
|
srCopy.Active = false
|
|
|
srCopy.Static = true
|
|
|
+ srCopy.Dynamic = false
|
|
|
liveRoutes = append(liveRoutes, srCopy)
|
|
|
}
|
|
|
}
|
|
|
@@ -200,23 +234,58 @@ func (m *RoutingManager) SaveRoute(r RouteItem) error {
|
|
|
if r.DstAddress == "" {
|
|
|
return fmt.Errorf("destination address is required")
|
|
|
}
|
|
|
+ if r.DstAddress == "default" {
|
|
|
+ r.DstAddress = "0.0.0.0/0"
|
|
|
+ }
|
|
|
if r.RoutingTable == "" {
|
|
|
r.RoutingTable = "main"
|
|
|
}
|
|
|
- if r.ID == "" {
|
|
|
- r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
|
- }
|
|
|
r.Static = true
|
|
|
|
|
|
- found := false
|
|
|
+ // 1. If old route details are provided from edit modal, remove old route first
|
|
|
+ if r.OldDstAddress != "" {
|
|
|
+ oldDist := 0
|
|
|
+ if r.OldDistance != nil {
|
|
|
+ oldDist = *r.OldDistance
|
|
|
+ }
|
|
|
+ oldRoute := RouteItem{
|
|
|
+ DstAddress: r.OldDstAddress,
|
|
|
+ Gateway: r.OldGateway,
|
|
|
+ RoutingTable: r.OldRoutingTable,
|
|
|
+ Distance: oldDist,
|
|
|
+ Interface: r.OldInterface,
|
|
|
+ }
|
|
|
+ _ = m.removeRouteFromKernel(oldRoute)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. If existing entry in m.staticRoutes exists, remove its old route from kernel
|
|
|
+ foundIndex := -1
|
|
|
for i, existing := range m.staticRoutes {
|
|
|
- if existing.ID == r.ID {
|
|
|
- m.staticRoutes[i] = r
|
|
|
- found = true
|
|
|
+ if existing.ID == r.ID || (r.ID == "" && existing.DstAddress == r.DstAddress && existing.RoutingTable == r.RoutingTable) {
|
|
|
+ foundIndex = i
|
|
|
+ _ = m.removeRouteFromKernel(existing)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
- if !found {
|
|
|
+
|
|
|
+ // 3. If r.ID starts with kr- (kernel static route being edited/adopted), remove it from kernel
|
|
|
+ if strings.HasPrefix(r.ID, "kr-") {
|
|
|
+ for _, lr := range m.listLiveRoutesLocked() {
|
|
|
+ if lr.ID == r.ID {
|
|
|
+ _ = m.removeRouteFromKernel(lr)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
|
+ }
|
|
|
+
|
|
|
+ if r.ID == "" {
|
|
|
+ r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
|
+ }
|
|
|
+
|
|
|
+ if foundIndex >= 0 {
|
|
|
+ m.staticRoutes[foundIndex] = r
|
|
|
+ } else {
|
|
|
m.staticRoutes = append(m.staticRoutes, r)
|
|
|
}
|
|
|
|
|
|
@@ -249,9 +318,19 @@ func (m *RoutingManager) DeleteRoute(id string) error {
|
|
|
|
|
|
if target != nil {
|
|
|
_ = m.removeRouteFromKernel(*target)
|
|
|
+ m.staticRoutes = updated
|
|
|
+ return m.save()
|
|
|
}
|
|
|
- m.staticRoutes = updated
|
|
|
- return m.save()
|
|
|
+
|
|
|
+ // If not in staticRoutes, could be a live route (e.g. kr-...)
|
|
|
+ for _, lr := range m.listLiveRoutesLocked() {
|
|
|
+ if lr.ID == id {
|
|
|
+ _ = m.removeRouteFromKernel(lr)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return fmt.Errorf("route not found")
|
|
|
}
|
|
|
|
|
|
func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
@@ -270,29 +349,47 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // If not in staticRoutes, could be a kernel static route to disable
|
|
|
+ if disabled {
|
|
|
+ for _, lr := range m.listLiveRoutesLocked() {
|
|
|
+ if lr.ID == id {
|
|
|
+ lr.Disabled = true
|
|
|
+ lr.Static = true
|
|
|
+ lr.Active = false
|
|
|
+ lr.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
|
+ _ = m.removeRouteFromKernel(lr)
|
|
|
+ m.staticRoutes = append(m.staticRoutes, lr)
|
|
|
+ return m.save()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
return fmt.Errorf("route not found")
|
|
|
}
|
|
|
|
|
|
func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
|
|
|
- args := []string{"route", "replace"}
|
|
|
-
|
|
|
- if r.DstAddress == "default" || r.DstAddress == "0.0.0.0/0" {
|
|
|
- args = append(args, "default")
|
|
|
- } else {
|
|
|
- args = append(args, r.DstAddress)
|
|
|
+ dst := strings.TrimSpace(r.DstAddress)
|
|
|
+ if dst == "default" || dst == "0.0.0.0/0" {
|
|
|
+ dst = "default"
|
|
|
+ } else if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
|
|
|
+ dst = dst + "/32"
|
|
|
}
|
|
|
|
|
|
- if r.Gateway != "" {
|
|
|
- // check if gateway is IP or device
|
|
|
- if net.ParseIP(r.Gateway) != nil {
|
|
|
- args = append(args, "via", r.Gateway)
|
|
|
+ args := []string{"route", "replace", dst}
|
|
|
+
|
|
|
+ gw := strings.TrimSpace(r.Gateway)
|
|
|
+ if gw != "" {
|
|
|
+ if net.ParseIP(gw) != nil {
|
|
|
+ args = append(args, "via", gw)
|
|
|
} else {
|
|
|
- args = append(args, "dev", r.Gateway)
|
|
|
+ args = append(args, "dev", gw)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if r.Interface != "" && r.Gateway != r.Interface {
|
|
|
- args = append(args, "dev", r.Interface)
|
|
|
+ iface := strings.TrimSpace(r.Interface)
|
|
|
+ if iface != "" && gw != iface && (gw == "" || net.ParseIP(gw) != nil) {
|
|
|
+ args = append(args, "dev", iface)
|
|
|
}
|
|
|
|
|
|
if r.Distance > 0 {
|
|
|
@@ -303,6 +400,8 @@ func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
|
|
|
args = append(args, "table", r.RoutingTable)
|
|
|
}
|
|
|
|
|
|
+ args = append(args, "proto", "static")
|
|
|
+
|
|
|
cmd := exec.Command("ip", args...)
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
if err != nil {
|
|
|
@@ -312,11 +411,26 @@ func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
|
|
|
}
|
|
|
|
|
|
func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
|
|
|
- args := []string{"route", "del"}
|
|
|
- if r.DstAddress == "default" || r.DstAddress == "0.0.0.0/0" {
|
|
|
- args = append(args, "default")
|
|
|
- } else {
|
|
|
- args = append(args, r.DstAddress)
|
|
|
+ dst := strings.TrimSpace(r.DstAddress)
|
|
|
+ if dst == "default" || dst == "0.0.0.0/0" {
|
|
|
+ dst = "default"
|
|
|
+ } else if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
|
|
|
+ dst = dst + "/32"
|
|
|
+ }
|
|
|
+
|
|
|
+ args := []string{"route", "del", dst}
|
|
|
+
|
|
|
+ gw := strings.TrimSpace(r.Gateway)
|
|
|
+ if gw != "" {
|
|
|
+ if net.ParseIP(gw) != nil {
|
|
|
+ args = append(args, "via", gw)
|
|
|
+ } else {
|
|
|
+ args = append(args, "dev", gw)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if r.Distance > 0 {
|
|
|
+ args = append(args, "metric", strconv.Itoa(r.Distance))
|
|
|
}
|
|
|
|
|
|
if r.RoutingTable != "" && r.RoutingTable != "main" {
|
|
|
@@ -324,6 +438,14 @@ func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
|
|
|
}
|
|
|
|
|
|
_ = exec.Command("ip", args...).Run()
|
|
|
+
|
|
|
+ // Fallback deletion without metric
|
|
|
+ fallbackArgs := []string{"route", "del", dst}
|
|
|
+ if r.RoutingTable != "" && r.RoutingTable != "main" {
|
|
|
+ fallbackArgs = append(fallbackArgs, "table", r.RoutingTable)
|
|
|
+ }
|
|
|
+ _ = exec.Command("ip", fallbackArgs...).Run()
|
|
|
+
|
|
|
return nil
|
|
|
}
|
|
|
|