package handlers import ( "context" "errors" "log/slog" "net" "strings" "github.com/gin-gonic/gin" "git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response" "git.netcell-it.de/projekte/edgeguard-native/internal/models" "git.netcell-it.de/projekte/edgeguard-native/internal/services/audit" dhcpsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/dhcp" ) // DHCPHandler exposes /api/v1/dhcp/{settings,subnets,reservations} for // the Kea DHCPv4 server. type DHCPHandler struct { Repo *dhcpsvc.Repo Audit *audit.Repo NodeID string Reloader func(ctx context.Context) error } func NewDHCPHandler(repo *dhcpsvc.Repo, a *audit.Repo, nodeID string, reloader func(context.Context) error) *DHCPHandler { return &DHCPHandler{Repo: repo, Audit: a, NodeID: nodeID, Reloader: reloader} } func (h *DHCPHandler) reload(ctx context.Context, op string) { if h.Reloader == nil { return } if err := h.Reloader(ctx); err != nil { slog.Warn("kea: reload after mutation failed", "op", op, "error", err) } } func (h *DHCPHandler) Register(rg *gin.RouterGroup) { g := rg.Group("/dhcp") g.GET("/settings", h.GetSettings) g.PUT("/settings", h.UpdateSettings) s := g.Group("/subnets") s.GET("", h.ListSubnets) s.POST("", h.CreateSubnet) s.GET("/:id", h.GetSubnet) s.PUT("/:id", h.UpdateSubnet) s.DELETE("/:id", h.DeleteSubnet) s.GET("/:id/reservations", h.ListReservationsForSubnet) s.POST("/:id/reservations", h.CreateReservation) r := g.Group("/reservations") r.GET("", h.ListAllReservations) r.GET("/:id", h.GetReservation) r.PUT("/:id", h.UpdateReservation) r.DELETE("/:id", h.DeleteReservation) } // ── Settings ───────────────────────────────────────────────────────── func (h *DHCPHandler) GetSettings(c *gin.Context) { s, err := h.Repo.GetSettings(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, s) } func (h *DHCPHandler) UpdateSettings(c *gin.Context) { var req models.DHCPSettings if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } if req.DefaultLease <= 0 { req.DefaultLease = 3600 } if req.MaxLease < req.DefaultLease { req.MaxLease = req.DefaultLease } if err := validateIPList(req.DNSServers); err != nil { response.BadRequest(c, err) return } out, err := h.Repo.UpdateSettings(c.Request.Context(), req) if err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dhcp.settings.update", "", gin.H{"enabled": out.Enabled}, h.NodeID) response.OK(c, out) h.reload(c.Request.Context(), "settings.update") } // ── Subnets ────────────────────────────────────────────────────────── func (h *DHCPHandler) ListSubnets(c *gin.Context) { out, err := h.Repo.ListSubnets(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"subnets": out}) } func (h *DHCPHandler) GetSubnet(c *gin.Context) { id, ok := parseID(c) if !ok { return } s, err := h.Repo.GetSubnet(c.Request.Context(), id) if err != nil { h.subnetErr(c, err) return } response.OK(c, s) } func (h *DHCPHandler) CreateSubnet(c *gin.Context) { var req models.DHCPSubnet if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } if err := h.validateSubnet(c, &req); err != nil { response.BadRequest(c, err) return } out, err := h.Repo.CreateSubnet(c.Request.Context(), req) if err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dhcp.subnet.create", out.Name, out, h.NodeID) response.Created(c, out) h.reload(c.Request.Context(), "subnet.create") } func (h *DHCPHandler) UpdateSubnet(c *gin.Context) { id, ok := parseID(c) if !ok { return } var req models.DHCPSubnet if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } if err := h.validateSubnet(c, &req); err != nil { response.BadRequest(c, err) return } out, err := h.Repo.UpdateSubnet(c.Request.Context(), id, req) if err != nil { h.subnetErr(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dhcp.subnet.update", out.Name, out, h.NodeID) response.OK(c, out) h.reload(c.Request.Context(), "subnet.update") } func (h *DHCPHandler) DeleteSubnet(c *gin.Context) { id, ok := parseID(c) if !ok { return } if err := h.Repo.DeleteSubnet(c.Request.Context(), id); err != nil { h.subnetErr(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dhcp.subnet.delete", "", gin.H{"id": id}, h.NodeID) response.OK(c, gin.H{"ok": true}) h.reload(c.Request.Context(), "subnet.delete") } // ── Reservations ───────────────────────────────────────────────────── func (h *DHCPHandler) ListAllReservations(c *gin.Context) { out, err := h.Repo.ListAllReservations(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"reservations": out}) } func (h *DHCPHandler) ListReservationsForSubnet(c *gin.Context) { id, ok := parseID(c) if !ok { return } out, err := h.Repo.ListReservationsForSubnet(c.Request.Context(), id) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"reservations": out}) } func (h *DHCPHandler) GetReservation(c *gin.Context) { id, ok := parseID(c) if !ok { return } v, err := h.Repo.GetReservation(c.Request.Context(), id) if err != nil { h.resvErr(c, err) return } response.OK(c, v) } func (h *DHCPHandler) CreateReservation(c *gin.Context) { subnetID, ok := parseID(c) if !ok { return } var req models.DHCPReservation if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } req.SubnetID = subnetID if err := validateReservation(&req); err != nil { response.BadRequest(c, err) return } out, err := h.Repo.CreateReservation(c.Request.Context(), req) if err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dhcp.reservation.create", out.MACAddress, out, h.NodeID) response.Created(c, out) h.reload(c.Request.Context(), "reservation.create") } func (h *DHCPHandler) UpdateReservation(c *gin.Context) { id, ok := parseID(c) if !ok { return } var req models.DHCPReservation if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err) return } if err := validateReservation(&req); err != nil { response.BadRequest(c, err) return } out, err := h.Repo.UpdateReservation(c.Request.Context(), id, req) if err != nil { h.resvErr(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dhcp.reservation.update", out.MACAddress, out, h.NodeID) response.OK(c, out) h.reload(c.Request.Context(), "reservation.update") } func (h *DHCPHandler) DeleteReservation(c *gin.Context) { id, ok := parseID(c) if !ok { return } if err := h.Repo.DeleteReservation(c.Request.Context(), id); err != nil { h.resvErr(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dhcp.reservation.delete", "", gin.H{"id": id}, h.NodeID) response.OK(c, gin.H{"ok": true}) h.reload(c.Request.Context(), "reservation.delete") } // ── Validation + error mapping ─────────────────────────────────────── func (h *DHCPHandler) validateSubnet(c *gin.Context, s *models.DHCPSubnet) error { s.Name = strings.TrimSpace(s.Name) s.InterfaceName = strings.TrimSpace(s.InterfaceName) if s.Name == "" { return errors.New("name ist erforderlich") } if s.InterfaceName == "" { return errors.New("interface_name ist erforderlich") } if exists, err := h.Repo.InterfaceExists(c.Request.Context(), s.InterfaceName); err == nil && !exists { return errors.New("interface_name existiert nicht: " + s.InterfaceName) } if _, _, err := net.ParseCIDR(s.SubnetCIDR); err != nil { return errors.New("subnet_cidr ist kein gültiges CIDR: " + s.SubnetCIDR) } if (s.PoolStart == "") != (s.PoolEnd == "") { return errors.New("pool_start und pool_end müssen beide gesetzt sein (oder beide leer)") } for _, ip := range []string{s.PoolStart, s.PoolEnd, s.Gateway} { if ip != "" && net.ParseIP(ip) == nil { return errors.New("ungültige IP-Adresse: " + ip) } } return validateIPList(s.DNSServers) } func validateReservation(r *models.DHCPReservation) error { r.MACAddress = strings.TrimSpace(strings.ToLower(r.MACAddress)) r.IPAddress = strings.TrimSpace(r.IPAddress) if _, err := net.ParseMAC(r.MACAddress); err != nil { return errors.New("mac_address ist ungültig: " + r.MACAddress) } if net.ParseIP(r.IPAddress) == nil { return errors.New("ip_address ist ungültig: " + r.IPAddress) } return nil } // validateIPList prüft eine optionale Komma-Liste von IPs. func validateIPList(csv string) error { for _, p := range strings.Split(csv, ",") { p = strings.TrimSpace(p) if p != "" && net.ParseIP(p) == nil { return errors.New("ungültige IP in dns_servers: " + p) } } return nil } func (h *DHCPHandler) subnetErr(c *gin.Context, err error) { if errors.Is(err, dhcpsvc.ErrSubnetNotFound) { response.NotFound(c, err) return } response.Internal(c, err) } func (h *DHCPHandler) resvErr(c *gin.Context, err error) { if errors.Is(err, dhcpsvc.ErrReservationNotFound) { response.NotFound(c, err) return } response.Internal(c, err) }