Store device FIB for observability (no rule limiting) #5
@@ -131,6 +131,10 @@ func Render(in Input) (*RenderedConfig, error) {
|
|||||||
|
|
||||||
usedSets := map[string]model.AddressGroup{}
|
usedSets := map[string]model.AddressGroup{}
|
||||||
|
|
||||||
|
// Every enforcing device carries every applicable rule: the interface-agnostic
|
||||||
|
// address-matched form is correct under ECMP precisely because it does not
|
||||||
|
// depend on which device is on the path (over-approximation is safe). Reported
|
||||||
|
// FIBs are stored for observability/validation, not to limit rules.
|
||||||
if out.Enforcing {
|
if out.Enforcing {
|
||||||
for _, rule := range in.Rules {
|
for _, rule := range in.Rules {
|
||||||
rr, err := renderRule(in, rule, usedSets)
|
rr, err := renderRule(in, rule, usedSets)
|
||||||
|
|||||||
@@ -116,6 +116,31 @@ func TestRenderUnknownGroupIsError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReportedFIBDoesNotLimitRules(t *testing.T) {
|
||||||
|
// A router with a narrow FIB must still carry every applicable rule: reported
|
||||||
|
// reachability is observability data, not a rule filter (over-approximation is
|
||||||
|
// safe and intended under ECMP).
|
||||||
|
in := Input{
|
||||||
|
Fabric: &model.Fabric{Name: "core", EnforceOnRouters: true},
|
||||||
|
Device: model.Device{Name: "rt1", Class: model.ClassRouter, Fabric: "core",
|
||||||
|
ReachablePrefixes: []string{"192.168.0.0/16"}},
|
||||||
|
Zones: map[string]model.Zone{
|
||||||
|
"zone-a": {Name: "zone-a", Subnets: []string{"10.1.0.0/24"}},
|
||||||
|
"zone-b": {Name: "zone-b", Subnets: []string{"10.4.0.0/24"}},
|
||||||
|
},
|
||||||
|
Rules: []model.Rule{
|
||||||
|
{ID: 1, Action: "accept", Source: []string{"zone-a"}, Dest: []string{"zone-b"}, Proto: "tcp", Ports: []string{"22"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cfg, err := Render(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Render: %v", err)
|
||||||
|
}
|
||||||
|
if len(cfg.Rules) != 1 {
|
||||||
|
t.Errorf("reported FIB must not limit rules, got %d", len(cfg.Rules))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEffectiveResolverPrefersDevice(t *testing.T) {
|
func TestEffectiveResolverPrefersDevice(t *testing.T) {
|
||||||
in := baseInput()
|
in := baseInput()
|
||||||
in.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall, Resolver: []string{"10.9.9.9"}}
|
in.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall, Resolver: []string{"10.9.9.9"}}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Per-device reachability, reported by the agent from its FIB. The compiler uses
|
||||||
|
-- it to scope which routers actually need to enforce an intent: a router only
|
||||||
|
-- carries a rule if it can route to both the source and destination networks.
|
||||||
|
-- When absent, the compiler safely over-approximates (enforces everywhere).
|
||||||
|
ALTER TABLE devices
|
||||||
|
ADD COLUMN reachable_prefixes JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
ADD COLUMN routes_reported_at TIMESTAMPTZ;
|
||||||
@@ -99,6 +99,10 @@ type Device struct {
|
|||||||
Fabric string `json:"fabric,omitempty"`
|
Fabric string `json:"fabric,omitempty"`
|
||||||
Resolver []string `json:"resolver,omitempty"`
|
Resolver []string `json:"resolver,omitempty"`
|
||||||
Settings map[string]string `json:"settings,omitempty"`
|
Settings map[string]string `json:"settings,omitempty"`
|
||||||
|
|
||||||
|
// ReachablePrefixes is the device's FIB as last reported by its agent
|
||||||
|
// (server-managed). The compiler uses it to scope router enforcement.
|
||||||
|
ReachablePrefixes []string `json:"reachable_prefixes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binding maps a global zone to one device's local interface(s).
|
// Binding maps a global zone to one device's local interface(s).
|
||||||
|
|||||||
@@ -350,6 +350,24 @@ func (s *Server) handleDeviceConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = w.Write(body)
|
_, _ = w.Write(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeviceRoutes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var body struct {
|
||||||
|
Prefixes []string `json:"prefixes"`
|
||||||
|
}
|
||||||
|
if !decode(w, r, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.UpdateDeviceRoutes(r.Context(), chi.URLParam(r, "name"), body.Prefixes); err != nil {
|
||||||
|
if errors.Is(err, store.ErrNotFound) {
|
||||||
|
writeError(w, http.StatusNotFound, "device not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleDeviceStatus(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDeviceStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
var body struct {
|
var body struct {
|
||||||
Generation int64 `json:"generation"`
|
Generation int64 `json:"generation"`
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ func (s *Server) routes() http.Handler {
|
|||||||
r.Use(s.requireToken(s.agentToken))
|
r.Use(s.requireToken(s.agentToken))
|
||||||
r.Get("/devices/{name}/config", s.handleDeviceConfig)
|
r.Get("/devices/{name}/config", s.handleDeviceConfig)
|
||||||
r.Post("/devices/{name}/status", s.handleDeviceStatus)
|
r.Post("/devices/{name}/status", s.handleDeviceStatus)
|
||||||
|
r.Post("/devices/{name}/routes", s.handleDeviceRoutes)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+26
-3
@@ -290,10 +290,11 @@ func (s *Store) RecordDeviceStatus(ctx context.Context, name string, generation
|
|||||||
|
|
||||||
func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error) {
|
func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error) {
|
||||||
var d model.Device
|
var d model.Device
|
||||||
var resolver, settings []byte
|
var resolver, settings, reachable []byte
|
||||||
err := s.pool.QueryRow(ctx,
|
err := s.pool.QueryRow(ctx,
|
||||||
`SELECT name, class, COALESCE(fabric, ''), resolver, settings FROM devices WHERE name = $1`, name,
|
`SELECT name, class, COALESCE(fabric, ''), resolver, settings, reachable_prefixes
|
||||||
).Scan(&d.Name, &d.Class, &d.Fabric, &resolver, &settings)
|
FROM devices WHERE name = $1`, name,
|
||||||
|
).Scan(&d.Name, &d.Class, &d.Fabric, &resolver, &settings, &reachable)
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return d, ErrNotFound
|
return d, ErrNotFound
|
||||||
}
|
}
|
||||||
@@ -303,9 +304,31 @@ func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error
|
|||||||
if err := json.Unmarshal(resolver, &d.Resolver); err != nil {
|
if err := json.Unmarshal(resolver, &d.Resolver); err != nil {
|
||||||
return d, err
|
return d, err
|
||||||
}
|
}
|
||||||
|
if err := json.Unmarshal(reachable, &d.ReachablePrefixes); err != nil {
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
return d, json.Unmarshal(settings, &d.Settings)
|
return d, json.Unmarshal(settings, &d.Settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDeviceRoutes stores the reachable prefixes an agent reports from its FIB.
|
||||||
|
// This is scoping data, not config — it does not bump the config generation.
|
||||||
|
func (s *Store) UpdateDeviceRoutes(ctx context.Context, name string, prefixes []string) error {
|
||||||
|
reachable, err := jsonb(prefixes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tag, err := s.pool.Exec(ctx,
|
||||||
|
`UPDATE devices SET reachable_prefixes = $2, routes_reported_at = now() WHERE name = $1`,
|
||||||
|
name, reachable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Settings, portgroups, policies ----------------------------------------
|
// ---- Settings, portgroups, policies ----------------------------------------
|
||||||
|
|
||||||
func (s *Store) GetSettings(ctx context.Context) (model.Settings, error) {
|
func (s *Store) GetSettings(ctx context.Context) (model.Settings, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user