package snapshot
import (
"context"
"fmt"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
rbacv3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
network_rbacv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
"github.com/envoyproxy/go-control-plane/pkg/cache/types"
resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const rbacFilterName = "envoy.filters.network.rbac"
// buildRBACFilter constructs a network RBAC filter that DENYs the given IPs.
// Returns nil when blockedIPs is empty (caller should strip any existing filter).
func buildRBACFilter(blockedIPs []string) (*listenerv3.Filter, error) {
if len(blockedIPs) == 0 {
return nil, nil
}
principals := make([]*rbacv3.Principal, 0, len(blockedIPs))
for _, ip := range blockedIPs {
principals = append(principals, &rbacv3.Principal{
Identifier: &rbacv3.Principal_RemoteIp{
RemoteIp: &corev3.CidrRange{
AddressPrefix: ip,
PrefixLen: wrapperspb.UInt32(32),
},
},
})
}
rbacConfig := &network_rbacv3.RBAC{
StatPrefix: "netwatch_block",
Rules: &rbacv3.RBAC{
Action: rbacv3.RBAC_DENY,
Policies: map[string]*rbacv3.Policy{
"blocked-ips": {
Permissions: []*rbacv3.Permission{
{Rule: &rbacv3.Permission_Any{Any: true}},
},
Principals: principals,
},
},
},
}
typedConfig, err := anypb.New(rbacConfig)
if err != nil {
return nil, fmt.Errorf("failed to marshal RBAC config: %w", err)
}
return &listenerv3.Filter{
Name: rbacFilterName,
ConfigType: &listenerv3.Filter_TypedConfig{TypedConfig: typedConfig},
}, nil
}
// InjectRBACIntoAllListeners prepends a DENY RBAC filter to every filter chain
// of every listener. If blockedIPs is empty, any existing RBAC filter is removed.
// The snapshot version is bumped so Envoy picks up the change immediately.
func (sm *SnapshotManager) InjectRBACIntoAllListeners(ctx context.Context, blockedIPs []string) error {
snap, err := sm.Cache.GetSnapshot(sm.NodeID)
if err != nil {
return fmt.Errorf("no active snapshot: %w", err)
}
rbacFilter, err := buildRBACFilter(blockedIPs)
if err != nil {
return err
}
// Copy all non-listener resources unchanged
resources := map[resourcev3.Type][]types.Resource{}
for _, rtype := range []resourcev3.Type{
resourcev3.ClusterType,
resourcev3.RouteType,
resourcev3.SecretType,
resourcev3.ExtensionConfigType,
} {
for _, v := range snap.GetResources(rtype) {
resources[rtype] = append(resources[rtype], v)
}
}
// Mutate listeners in-place (they're already cloned from DB on each update)
for _, item := range snap.GetResources(resourcev3.ListenerType) {
listener, ok := item.(*listenerv3.Listener)
if !ok {
resources[resourcev3.ListenerType] = append(resources[resourcev3.ListenerType], item)
continue
}
for _, fc := range listener.GetFilterChains() {
// Strip any previous RBAC filter
stripped := make([]*listenerv3.Filter, 0, len(fc.Filters))
for _, f := range fc.Filters {
if f.GetName() != rbacFilterName {
stripped = append(stripped, f)
}
}
if rbacFilter != nil {
fc.Filters = append([]*listenerv3.Filter{rbacFilter}, stripped...)
} else {
fc.Filters = stripped
}
}
resources[resourcev3.ListenerType] = append(resources[resourcev3.ListenerType], listener)
}
newSnap, err := cachev3.NewSnapshot(
fmt.Sprintf("rbac-%d", len(blockedIPs)),
resources,
)
if err != nil {
return fmt.Errorf("failed to create RBAC snapshot: %w", err)
}
return sm.Cache.SetSnapshot(ctx, sm.NodeID, newSnap)
}