diff --git a/internal/plumbing/ebpf/prog/usid.c b/internal/plumbing/ebpf/prog/usid.c index cefaeb29..3117b164 100644 --- a/internal/plumbing/ebpf/prog/usid.c +++ b/internal/plumbing/ebpf/prog/usid.c @@ -134,6 +134,7 @@ static long (*bpf_redirect_neigh)(__u32 ifindex, struct bpf_redir_neigh *params, __u64 flags) = (void *) BPF_FUNC_redirect_neigh; static __u64 (*bpf_ktime_get_ns)(void) = (void *) BPF_FUNC_ktime_get_ns; +static __u64 (*bpf_ktime_get_boot_ns)(void) = (void *) BPF_FUNC_ktime_get_boot_ns; // vip_xlat_table's rewrite is a genuine address and port substitution with no // checksum-neutral shortcut, so it needs the incremental L4 checksum update any @@ -607,6 +608,9 @@ struct service_route_value { __u64 producer_token; __u8 grant_id[16]; __u8 target_sid[16]; + __u8 backend_addr[16]; + __u64 authorization_deadline_ns; + __s64 authorization_valid_until_ns; }; #define SERVICE_ROUTE_MODE_LOCAL 1 @@ -623,6 +627,7 @@ struct service_tunnel_header { __u8 direction; __u8 reserved; __u8 grant_id[16]; + __u8 frontend_addr[16]; } __attribute__((packed)); // service_access_key authorizes one transport endpoint on one consumer @@ -682,6 +687,7 @@ struct service_reverse_value { __u64 producer_token; __u8 grant_id[16]; __u8 return_sid[16]; + __u8 frontend_addr[16]; __u64 last_seen_ns; }; @@ -696,7 +702,10 @@ struct service_remote_grant_key { struct service_remote_grant_value { __u8 consumer_sid[16]; + __u8 frontend_addr[16]; __u64 producer_token; + __u64 authorization_deadline_ns; + __s64 authorization_valid_until_ns; }; struct service_policy_state_value { @@ -737,6 +746,7 @@ struct service_fib_scratch_value { __u8 tunnel_grant_id[16]; __u8 tunnel_return_sid[16]; __u8 tunnel_target_sid[16]; + __u8 tunnel_frontend_addr[16]; __u8 tunnel_direction; struct bpf_redir_neigh neigh; }; @@ -2167,7 +2177,9 @@ static USID_NOINLINE int service_ingress_authorize(struct __sk_buff *skb, __be16 __builtin_memcpy(s->remote_grant.addr, s->dest_addr, 16); struct service_remote_grant_value *grant = bpf_map_lookup_elem(&service_remote_grant_table, &s->remote_grant); - if (!grant || !service_id_equal(grant->consumer_sid, s->tunnel_return_sid) || + if (!grant || (grant->authorization_deadline_ns && bpf_ktime_get_boot_ns() >= grant->authorization_deadline_ns) || + !service_id_equal(grant->consumer_sid, s->tunnel_return_sid) || + !service_id_equal(grant->frontend_addr, s->tunnel_frontend_addr) || !service_identity_matches(ifindex, grant->producer_token)) return -1; __builtin_memset(&s->reverse, 0, sizeof(s->reverse)); @@ -2183,6 +2195,7 @@ static USID_NOINLINE int service_ingress_authorize(struct __sk_buff *skb, __be16 s->reverse_value.producer_token = grant->producer_token; __builtin_memcpy(s->reverse_value.grant_id, s->tunnel_grant_id, 16); __builtin_memcpy(s->reverse_value.return_sid, s->tunnel_return_sid, 16); + __builtin_memcpy(s->reverse_value.frontend_addr, s->tunnel_frontend_addr, 16); return service_install_reverse(s) ? -1 : 1; } @@ -2200,7 +2213,8 @@ static USID_NOINLINE int service_ingress_authorize(struct __sk_buff *skb, __be16 s->access.port = s->source_port; __builtin_memcpy(s->access.addr, s->source_addr, 16); struct service_access_value *access = bpf_map_lookup_elem(&service_access_table, &s->access); - if (!route || route->mode != SERVICE_ROUTE_MODE_REMOTE || + if (!route || (route->authorization_deadline_ns && bpf_ktime_get_boot_ns() >= route->authorization_deadline_ns) || + route->mode != SERVICE_ROUTE_MODE_REMOTE || !service_id_equal(route->grant_id, s->tunnel_grant_id) || !service_id_equal(route->target_sid, s->tunnel_return_sid) || !access || access->attachment_token != route->consumer_token || @@ -2358,6 +2372,7 @@ int usid_ingress(struct __sk_buff *skb) return TC_ACT_SHOT; } __builtin_memcpy(service_scratch->tunnel_grant_id, tunnel->grant_id, 16); + __builtin_memcpy(service_scratch->tunnel_frontend_addr, tunnel->frontend_addr, 16); __builtin_memcpy(service_scratch->tunnel_return_sid, ip6->saddr, 16); service_scratch->tunnel_direction = tunnel->direction; } @@ -2927,6 +2942,7 @@ static USID_NOINLINE long service_remote_encap(struct __sk_buff *skb, tunnel->inner_nexthdr = scratch->meta_family == USID_EGRESS_ROUTE_FAMILY_INET6 ? USID_IPPROTO_IPV6 : USID_IPPROTO_IPIP; tunnel->direction = scratch->tunnel_direction; __builtin_memcpy(tunnel->grant_id, scratch->tunnel_grant_id, 16); + __builtin_memcpy(tunnel->frontend_addr, scratch->tunnel_frontend_addr, 16); eth->h_proto = __builtin_bswap16(USID_ETH_P_IPV6); __builtin_memcpy(eth->h_dest, uplink->dmac, 6); __builtin_memcpy(eth->h_source, uplink->smac, 6); @@ -2958,9 +2974,10 @@ static USID_NOINLINE int service_install_reverse(struct service_fib_scratch_valu s->route.family = s->reverse.family; s->route.protocol = s->reverse.protocol; s->route.port = s->reverse.source_port; - __builtin_memcpy(s->route.addr, s->reverse.source_addr, 16); + __builtin_memcpy(s->route.addr, old->frontend_addr, 16); struct service_route_value *old_route = bpf_map_lookup_elem(&service_route_table, &s->route); - if (!old_route || old_route->mode != SERVICE_ROUTE_MODE_LOCAL || + if (!old_route || (old_route->authorization_deadline_ns && bpf_ktime_get_boot_ns() >= old_route->authorization_deadline_ns) || + old_route->mode != SERVICE_ROUTE_MODE_LOCAL || old_route->target_ifindex != s->reverse.ingress_ifindex || old_route->consumer_token != old->consumer_token || old_route->producer_token != old->producer_token || @@ -2980,7 +2997,8 @@ static USID_NOINLINE int service_install_reverse(struct service_fib_scratch_valu __builtin_memcpy(s->remote_grant.addr, s->reverse.source_addr, 16); struct service_remote_grant_value *old_grant = bpf_map_lookup_elem(&service_remote_grant_table, &s->remote_grant); - if (!old_grant || old_grant->producer_token != old->producer_token || + if (!old_grant || (old_grant->authorization_deadline_ns && bpf_ktime_get_boot_ns() >= old_grant->authorization_deadline_ns) || + old_grant->producer_token != old->producer_token || !service_id_equal(old_grant->consumer_sid, old->return_sid) || !service_identity_matches(s->reverse.ingress_ifindex, old_grant->producer_token)) { bpf_map_delete_elem(&service_reverse_table, &s->reverse); @@ -3007,6 +3025,45 @@ static USID_ALWAYS_INLINE int service_policy_enabled(void) return state && state->enabled; } +static USID_ALWAYS_INLINE int service_rewrite_address(struct __sk_buff *skb, + struct service_fib_scratch_value *s, int source, const __u8 replacement[16]) +{ + __u8 *current = source ? s->source_addr : s->dest_addr; + __u32 addr_off; + __u32 csum_off = s->l4_offset + (s->protocol == USID_IPPROTO_TCP ? + USID_TCP_CSUM_OFFSET : USID_UDP_CSUM_OFFSET); + __u64 flags = USID_BPF_F_PSEUDO_HDR | 4; + if (s->protocol == USID_IPPROTO_UDP) + flags |= USID_BPF_F_MARK_MANGLED_0; + if (s->meta_family == USID_EGRESS_ROUTE_FAMILY_INET6) { + addr_off = USID_L3_OFFSET + (source ? USID_OFFSETOF(struct usid_ip6hdr, saddr) : + USID_OFFSETOF(struct usid_ip6hdr, daddr)); +#pragma unroll + for (int i = 0; i < 4; i++) { + __u32 from, to; + __builtin_memcpy(&from, current + i * 4, 4); + __builtin_memcpy(&to, replacement + i * 4, 4); + if (bpf_l4_csum_replace(skb, csum_off, from, to, flags)) + return -1; + } + if (bpf_skb_store_bytes(skb, addr_off, replacement, 16, 0)) + return -1; + __builtin_memcpy(current, replacement, 16); + return 0; + } + addr_off = USID_L3_OFFSET + (source ? USID_OFFSETOF(struct usid_iphdr, saddr) : + USID_OFFSETOF(struct usid_iphdr, daddr)); + __u32 from, to; + __builtin_memcpy(&from, current, 4); + __builtin_memcpy(&to, replacement, 4); + if (bpf_l4_csum_replace(skb, csum_off, from, to, flags) || + bpf_l3_csum_replace(skb, USID_L3_OFFSET + USID_OFFSETOF(struct usid_iphdr, check), from, to, 4) || + bpf_skb_store_bytes(skb, addr_off, replacement, 4, 0)) + return -1; + __builtin_memcpy(current, replacement, 4); + return 0; +} + // service_path is noinline so its transient tuple-building scalars do not // inflate usid_egress's already tight stack on the unrelated encapsulation and // Packet-Too-Big call chain. @@ -3085,19 +3142,29 @@ static USID_NOINLINE long service_path(struct __sk_buff *skb, __be16 h_proto, __ __builtin_memcpy(s->remote_grant.grant_id, reverse->grant_id, 16); __builtin_memcpy(s->remote_grant.addr, s->source_addr, 16); struct service_remote_grant_value *grant = bpf_map_lookup_elem(&service_remote_grant_table, &s->remote_grant); - if (!grant || !service_id_equal(grant->consumer_sid, reverse->return_sid) || + if (!grant || (grant->authorization_deadline_ns && bpf_ktime_get_boot_ns() >= grant->authorization_deadline_ns) || + !service_id_equal(grant->consumer_sid, reverse->return_sid) || !service_identity_matches(ifindex, grant->producer_token)) { count_drop(DROP_REASON_SERVICE_ROUTE_DENIED); return TC_ACT_SHOT; } + if (service_rewrite_address(skb, s, 1, reverse->frontend_addr)) { + count_drop(DROP_REASON_SERVICE_ROUTE_REDIRECT_FAILED); + return TC_ACT_SHOT; + } __builtin_memcpy(s->tunnel_target_sid, reverse->return_sid, 16); __builtin_memcpy(s->tunnel_grant_id, reverse->grant_id, 16); + __builtin_memcpy(s->tunnel_frontend_addr, reverse->frontend_addr, 16); s->tunnel_direction = SERVICE_TUNNEL_REPLY; reverse->last_seen_ns = now; long rc = service_remote_encap(skb, s); if (rc != TC_ACT_REDIRECT) count_drop(DROP_REASON_SERVICE_ROUTE_REDIRECT_FAILED); return rc == TC_ACT_REDIRECT ? rc : TC_ACT_SHOT; } + if (service_rewrite_address(skb, s, 1, reverse->frontend_addr)) { + count_drop(DROP_REASON_SERVICE_ROUTE_REDIRECT_FAILED); + return TC_ACT_SHOT; + } __u32 consumer_ifindex = reverse->consumer_ifindex; USID_BARRIER_VAR(consumer_ifindex); __builtin_memset(&s->access, 0, sizeof(s->access)); @@ -3115,7 +3182,8 @@ static USID_NOINLINE long service_path(struct __sk_buff *skb, __be16 h_proto, __ __builtin_memcpy(s->route.addr, s->source_addr, 16); struct service_route_value *reply_route = bpf_map_lookup_elem(&service_route_table, &s->route); if (!access || access->attachment_token != reverse->consumer_token || !reply_route || - reply_route->mode != SERVICE_ROUTE_MODE_LOCAL || reply_route->target_ifindex != ifindex || + (reply_route->authorization_deadline_ns && bpf_ktime_get_boot_ns() >= reply_route->authorization_deadline_ns) || + reply_route->mode != SERVICE_ROUTE_MODE_LOCAL || reply_route->target_ifindex != ifindex || reply_route->consumer_token != reverse->consumer_token || reply_route->producer_token != reverse->producer_token) { count_drop(DROP_REASON_SERVICE_ROUTE_DENIED); @@ -3159,7 +3227,8 @@ static USID_NOINLINE long service_path(struct __sk_buff *skb, __be16 h_proto, __ count_drop(DROP_REASON_SERVICE_ROUTE_DENIED); return TC_ACT_SHOT; } - if (!service_identity_matches(ifindex, route->consumer_token) || + if ((route->authorization_deadline_ns && bpf_ktime_get_boot_ns() >= route->authorization_deadline_ns) || + !service_identity_matches(ifindex, route->consumer_token) || (route->mode == SERVICE_ROUTE_MODE_LOCAL && !service_identity_matches(route->target_ifindex, route->producer_token))) { count_drop(DROP_REASON_SERVICE_ROUTE_DENIED); @@ -3177,11 +3246,18 @@ static USID_NOINLINE long service_path(struct __sk_buff *skb, __be16 h_proto, __ count_drop(DROP_REASON_SERVICE_ROUTE_DENIED); return TC_ACT_SHOT; } + __u8 frontend_addr[16]; + __builtin_memcpy(frontend_addr, s->dest_addr, 16); + if (service_rewrite_address(skb, s, 0, route->backend_addr)) { + count_drop(DROP_REASON_SERVICE_ROUTE_REDIRECT_FAILED); + return TC_ACT_SHOT; + } if (route->mode == SERVICE_ROUTE_MODE_REMOTE) { __builtin_memcpy(s->tunnel_target_sid, route->target_sid, 16); __builtin_memcpy(s->tunnel_grant_id, route->grant_id, 16); s->tunnel_direction = SERVICE_TUNNEL_REQUEST; + __builtin_memcpy(s->tunnel_frontend_addr, frontend_addr, 16); long rc = service_remote_encap(skb, s); if (rc != TC_ACT_REDIRECT) count_drop(DROP_REASON_SERVICE_ROUTE_REDIRECT_FAILED); return rc == TC_ACT_REDIRECT ? rc : TC_ACT_SHOT; @@ -3201,6 +3277,7 @@ static USID_NOINLINE long service_path(struct __sk_buff *skb, __be16 h_proto, __ s->reverse_value.mode = SERVICE_ROUTE_MODE_LOCAL; s->reverse_value.consumer_token = route->consumer_token; s->reverse_value.producer_token = route->producer_token; + __builtin_memcpy(s->reverse_value.frontend_addr, frontend_addr, 16); if (service_install_reverse(s)) { count_drop(DROP_REASON_SERVICE_ROUTE_REDIRECT_FAILED); return TC_ACT_SHOT; diff --git a/internal/plumbing/ebpf/prog/usid_lease_linux_test.go b/internal/plumbing/ebpf/prog/usid_lease_linux_test.go new file mode 100644 index 00000000..68f53f4f --- /dev/null +++ b/internal/plumbing/ebpf/prog/usid_lease_linux_test.go @@ -0,0 +1,215 @@ +//go:build linux + +// Copyright 2026 Datum Cloud, Inc. +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package prog + +import ( + "math/bits" + "net/netip" + "testing" + "time" + + "golang.org/x/sys/unix" +) + +func TestUsidServiceEgress_AuthorizationExpiryRevokesEstablishedFlow(t *testing.T) { + requireRoot(t) + for _, mode := range []uint8{1, 2} { + name := "Local" + if mode == 2 { + name = "RemoteConsumer" + } + t.Run(name, func(t *testing.T) { + objs := loadObjects(t) + setServiceIdentity(t, objs, 1) + setUpEgressRouteAttachment(t, objs, 0xABCDEF, 0x100, 7) + setUpNodeSIDBase(t, objs, netip.MustParseAddr("fd00:1:2:3::"), 0x100) + if err := objs.PublicUplinkTable.Put(uint32(0), UsidPublicUplinkValue{LinkIfindex: 1}); err != nil { + t.Fatal(err) + } + service, consumer := netip.MustParseAddr("fd20:70::100"), netip.MustParseAddr("fd20:70::2") + value := UsidServiceRouteValue{TargetIfindex: 1, Mode: mode, ConsumerToken: 1, ProducerToken: 1, + GrantId: [16]byte{1}, TargetSid: netip.MustParseAddr("fd00:9:8:7:e000::").As16(), + BackendAddr: service.As16(), AuthorizationDeadlineNs: ^uint64(0)} + key := serviceRouteKey(service) + if err := objs.ServiceRouteTable.Put(key, value); err != nil { + t.Fatal(err) + } + if err := objs.ServiceAccessTable.Put(UsidServiceAccessKey{IngressIfindex: 1, Family: egressRouteFamilyINET6, + Protocol: 6, Port: bits.ReverseBytes16(8443), Addr: service.As16()}, + UsidServiceAccessValue{AttachmentToken: 1}); err != nil { + t.Fatal(err) + } + request := buildPlainV6PacketWithL4Ports(t, consumer, service, 49152, 8443) + reply := buildPlainV6PacketWithL4Ports(t, service, consumer, 8443, 49152) + if ret, _, err := objs.UsidServiceEgress.Test(request); err != nil || ret != tcActRedirect { + t.Fatalf("authorized request=%d, err=%v", ret, err) + } + if mode == 1 { + if ret, _, err := objs.UsidServiceEgress.Test(reply); err != nil || ret != tcActRedirect { + t.Fatalf("authorized reply=%d, err=%v", ret, err) + } + } + value.AuthorizationDeadlineNs = 1 + if err := objs.ServiceRouteTable.Put(key, value); err != nil { + t.Fatal(err) + } + if ret, _, err := objs.UsidServiceEgress.Test(request); err != nil || ret != tcActShot { + t.Fatalf("expired request=%d, err=%v, want drop", ret, err) + } + if mode == 1 { + if ret, _, err := objs.UsidServiceEgress.Test(reply); err != nil || ret != tcActShot { + t.Fatalf("expired established reply=%d, err=%v, want drop", ret, err) + } + } + value.AuthorizationDeadlineNs = ^uint64(0) + if err := objs.ServiceRouteTable.Put(key, value); err != nil { + t.Fatal(err) + } + if ret, _, err := objs.UsidServiceEgress.Test(request); err != nil || ret != tcActRedirect { + t.Fatalf("renewed request=%d, err=%v", ret, err) + } + }) + } +} + +func TestUsidServiceEgress_RemoteProducerExpiryRevokesEstablishedReply(t *testing.T) { + requireRoot(t) + objs := loadObjects(t) + setServiceIdentity(t, objs, 1) + setUpEgressRouteAttachment(t, objs, 0xABCDEF, 0x100, 7) + setUpNodeSIDBase(t, objs, netip.MustParseAddr("fd00:1:2:3::"), 0x100) + if err := objs.PublicUplinkTable.Put(uint32(0), UsidPublicUplinkValue{LinkIfindex: 1}); err != nil { + t.Fatal(err) + } + service, consumer := netip.MustParseAddr("fd20:70::100"), netip.MustParseAddr("fd20:70::2") + consumerSID := netip.MustParseAddr("fd00:9:8:7:e000::").As16() + grantID := [16]byte{1} + grantKey := UsidServiceRemoteGrantKey{ProducerIfindex: 1, Family: egressRouteFamilyINET6, Protocol: 6, + Port: bits.ReverseBytes16(8443), GrantId: grantID, Addr: service.As16()} + grant := UsidServiceRemoteGrantValue{ProducerToken: 1, ConsumerSid: consumerSID, + FrontendAddr: service.As16(), AuthorizationDeadlineNs: ^uint64(0)} + if err := objs.ServiceRemoteGrantTable.Put(grantKey, grant); err != nil { + t.Fatal(err) + } + // The remote ingress path normally establishes this row after granting the request. + var now unix.Timespec + if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil { + t.Fatal(err) + } + reverseKey := UsidServiceReverseKey{IngressIfindex: 1, Family: egressRouteFamilyINET6, Protocol: 6, + SourcePort: bits.ReverseBytes16(8443), DestPort: bits.ReverseBytes16(49152), SourceAddr: service.As16(), + DestAddr: consumer.As16()} + if err := objs.ServiceReverseTable.Put(reverseKey, UsidServiceReverseValue{Mode: 2, ProducerToken: 1, GrantId: grantID, + ReturnSid: consumerSID, FrontendAddr: service.As16(), LastSeenNs: uint64(now.Nano())}); err != nil { + t.Fatal(err) + } + reply := buildPlainV6PacketWithL4Ports(t, service, consumer, 8443, 49152) + if ret, _, err := objs.UsidServiceEgress.Test(reply); err != nil || ret != tcActRedirect { + t.Fatalf("authorized remote reply=%d err=%v", ret, err) + } + grant.AuthorizationDeadlineNs = 1 + if err := objs.ServiceRemoteGrantTable.Put(grantKey, grant); err != nil { + t.Fatal(err) + } + if ret, _, err := objs.UsidServiceEgress.Test(reply); err != nil || ret != tcActShot { + t.Fatalf("expired established remote reply=%d err=%v, want drop", ret, err) + } +} + +func TestUsidServiceEgress_TranslatedCollisionRequiresExpiredOwner(t *testing.T) { + requireRoot(t) + for _, expired := range []bool{false, true} { + name := "LiveOwner" + if expired { + name = "ExpiredOwner" + } + t.Run(name, func(t *testing.T) { + objs := loadObjects(t) + setServiceIdentity(t, objs, 1) + if err := objs.AttachmentIdentityTable.Put(uint32(2), uint64(2)); err != nil { + t.Fatal(err) + } + frontend := netip.MustParseAddr("fd20:70::53") + backend := netip.MustParseAddr("fd20:70::100") + consumer := netip.MustParseAddr("fd20:70::2") + newKey := serviceRouteKey(frontend) + value := UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1, + BackendAddr: backend.As16()} + if err := objs.ServiceRouteTable.Put(newKey, value); err != nil { + t.Fatal(err) + } + oldKey := newKey + oldKey.IngressIfindex = 2 + oldValue := value + oldValue.ConsumerToken = 2 + if expired { + oldValue.AuthorizationDeadlineNs = 1 + } + if err := objs.ServiceRouteTable.Put(oldKey, oldValue); err != nil { + t.Fatal(err) + } + if err := objs.ServiceAccessTable.Put(UsidServiceAccessKey{IngressIfindex: 1, Family: egressRouteFamilyINET6, + Protocol: 6, Port: bits.ReverseBytes16(8443), Addr: frontend.As16()}, + UsidServiceAccessValue{AttachmentToken: 1}); err != nil { + t.Fatal(err) + } + var now unix.Timespec + if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil { + t.Fatal(err) + } + reverseKey := UsidServiceReverseKey{IngressIfindex: 1, Family: egressRouteFamilyINET6, Protocol: 6, + SourcePort: bits.ReverseBytes16(8443), DestPort: bits.ReverseBytes16(49152), SourceAddr: backend.As16(), + DestAddr: consumer.As16()} + if err := objs.ServiceReverseTable.Put(reverseKey, UsidServiceReverseValue{ConsumerIfindex: 2, Mode: 1, + ConsumerToken: 2, + ProducerToken: 1, FrontendAddr: frontend.As16(), LastSeenNs: uint64(now.Nano())}); err != nil { + t.Fatal(err) + } + pkt := buildPlainV6PacketWithL4Ports(t, consumer, frontend, 49152, 8443) + want := uint32(tcActShot) + if expired { + want = tcActRedirect + } + if ret, _, err := objs.UsidServiceEgress.Test(pkt); err != nil || ret != want { + t.Fatalf("collision verdict=%d err=%v, want %d", ret, err, want) + } + }) + } +} + +func TestUsidServiceEgress_LeaseExpiresWithoutController(t *testing.T) { + requireRoot(t) + objs := loadObjects(t) + setServiceIdentity(t, objs, 1) + service, consumer := netip.MustParseAddr("fd20:70::100"), netip.MustParseAddr("fd20:70::2") + var now unix.Timespec + if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &now); err != nil { + t.Fatal(err) + } + deadline := uint64(now.Nano()) + uint64(time.Second) + if err := objs.ServiceRouteTable.Put(serviceRouteKey(service), UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, + ConsumerToken: 1, ProducerToken: 1, BackendAddr: service.As16(), AuthorizationDeadlineNs: deadline}); err != nil { + t.Fatal(err) + } + if err := objs.ServiceAccessTable.Put(UsidServiceAccessKey{IngressIfindex: 1, Family: egressRouteFamilyINET6, + Protocol: 6, Port: bits.ReverseBytes16(8443), Addr: service.As16()}, + UsidServiceAccessValue{AttachmentToken: 1}); err != nil { + t.Fatal(err) + } + request := buildPlainV6PacketWithL4Ports(t, consumer, service, 49152, 8443) + reply := buildPlainV6PacketWithL4Ports(t, service, consumer, 8443, 49152) + if ret, _, err := objs.UsidServiceEgress.Test(request); err != nil || ret != tcActRedirect { + t.Fatalf("before expiry=%d err=%v", ret, err) + } + time.Sleep(1100 * time.Millisecond) + // No control-plane or map mutation occurs between the live and expired packets. + for _, pkt := range [][]byte{request, reply} { + if ret, _, err := objs.UsidServiceEgress.Test(pkt); err != nil || ret != tcActShot { + t.Fatalf("after expiry=%d err=%v, want drop", ret, err) + } + } +} diff --git a/internal/plumbing/ebpf/prog/usid_test.go b/internal/plumbing/ebpf/prog/usid_test.go index 3c252e5d..284a2add 100644 --- a/internal/plumbing/ebpf/prog/usid_test.go +++ b/internal/plumbing/ebpf/prog/usid_test.go @@ -1525,10 +1525,11 @@ func TestUsidEgress_ServiceRouteAllowsAuthorizedPort(t *testing.T) { setServiceIdentity(t, objs, 1) service := netip.MustParseAddr("fd20:70::100") + frontend := netip.MustParseAddr("fd53::53") if err := objs.ServiceRouteTable.Put( - serviceRouteKey(service), + serviceRouteKey(frontend), UsidServiceRouteValue{ - TargetIfindex: loopback, Mode: 1, ConsumerToken: 1, ProducerToken: 1, + TargetIfindex: loopback, Mode: 1, ConsumerToken: 1, ProducerToken: 1, BackendAddr: service.As16(), }, ); err != nil { t.Fatalf("populate service_route_table: %v", err) @@ -1538,13 +1539,13 @@ func TestUsidEgress_ServiceRouteAllowsAuthorizedPort(t *testing.T) { Family: egressRouteFamilyINET6, Protocol: 6, Port: bits.ReverseBytes16(servicePort), - Addr: service.As16(), + Addr: frontend.As16(), }, UsidServiceAccessValue{AttachmentToken: 1}); err != nil { t.Fatalf("populate service_access_table: %v", err) } pkt := buildPlainV6PacketWithL4Ports(t, - netip.MustParseAddr("fd20:70::2"), service, 49152, servicePort) + netip.MustParseAddr("fd20:70::2"), frontend, 49152, servicePort) ret, out, err := objs.UsidServiceEgress.Test(pkt) if err != nil { t.Fatalf("program test-run: %v", err) @@ -1585,7 +1586,7 @@ func TestUsidEgress_ServiceRouteDropsUnauthorizedPort(t *testing.T) { service := netip.MustParseAddr("fd20:70::100") if err := objs.ServiceRouteTable.Put( serviceRouteKey(service), - UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1}, + UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1, BackendAddr: service.As16()}, ); err != nil { t.Fatalf("populate service_route_table: %v", err) } @@ -1622,7 +1623,8 @@ func TestUsidServiceEgress_RemoteRequestPreservesInnerSource(t *testing.T) { t.Fatalf("populate public uplink: %v", err) } if err := objs.ServiceRouteTable.Put(serviceRouteKey(service), - UsidServiceRouteValue{Mode: 2, ConsumerToken: 1, GrantId: grantID, TargetSid: targetSID.As16()}); err != nil { + UsidServiceRouteValue{Mode: 2, ConsumerToken: 1, GrantId: grantID, TargetSid: targetSID.As16(), + BackendAddr: service.As16()}); err != nil { t.Fatalf("populate remote service route: %v", err) } if err := objs.ServiceAccessTable.Put(UsidServiceAccessKey{IngressIfindex: 1, Family: egressRouteFamilyINET6, @@ -1635,8 +1637,8 @@ func TestUsidServiceEgress_RemoteRequestPreservesInnerSource(t *testing.T) { if err != nil || ret != tcActRedirect { t.Fatalf("remote request verdict = %d, err = %v; want TC_ACT_REDIRECT", ret, err) } - if len(out) != len(pkt)+60 { - t.Fatalf("encapsulated length = %d, want %d", len(out), len(pkt)+60) + if len(out) != len(pkt)+76 { + t.Fatalf("encapsulated length = %d, want %d", len(out), len(pkt)+76) } if got := netip.AddrFrom16([16]byte(out[22:38])); got != wantOuterSource { t.Errorf("outer source = %s, want %s", got, wantOuterSource) @@ -1647,10 +1649,10 @@ func TestUsidServiceEgress_RemoteRequestPreservesInnerSource(t *testing.T) { if out[54] != 1 || out[55] != 41 || out[56] != 1 || !bytes.Equal(out[58:74], grantID[:]) { t.Errorf("service shim = % x, want version=1 inner=41 request=1 grant=%x", out[54:74], grantID) } - if got := netip.AddrFrom16([16]byte(out[82:98])); got != consumer { + if got := netip.AddrFrom16([16]byte(out[98:114])); got != consumer { t.Errorf("inner source = %s, want original consumer %s", got, consumer) } - if got := netip.AddrFrom16([16]byte(out[98:114])); got != service { + if got := netip.AddrFrom16([16]byte(out[114:130])); got != service { t.Errorf("inner destination = %s, want endpoint %s", got, service) } } @@ -1816,7 +1818,7 @@ func TestUsidServiceEgress_ReplyRequiresAuthorizedForwardFlow(t *testing.T) { } if err := objs.ServiceRouteTable.Put( serviceRouteKey(service), - UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1}, + UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1, BackendAddr: service.As16()}, ); err != nil { t.Fatalf("populate service_route_table: %v", err) } @@ -1844,7 +1846,7 @@ func TestUsidServiceEgress_ReplyRequiresAuthorizedForwardFlow(t *testing.T) { if err := objs.ServiceRouteTable.Put( serviceRouteKey(service), - UsidServiceRouteValue{TargetIfindex: 2, Mode: 1, ConsumerToken: 1, ProducerToken: 1}, + UsidServiceRouteValue{TargetIfindex: 2, Mode: 1, ConsumerToken: 1, ProducerToken: 1, BackendAddr: service.As16()}, ); err != nil { t.Fatalf("replace service route target: %v", err) } @@ -1853,7 +1855,7 @@ func TestUsidServiceEgress_ReplyRequiresAuthorizedForwardFlow(t *testing.T) { } if err := objs.ServiceRouteTable.Put( serviceRouteKey(service), - UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1}, + UsidServiceRouteValue{TargetIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1, BackendAddr: service.As16()}, ); err != nil { t.Fatalf("restore service route target: %v", err) } @@ -1876,7 +1878,7 @@ func TestUsidServiceEgress_ReclaimsCollisionFromStaleRemoteGrant(t *testing.T) { grantID := [16]byte{9} if err := objs.ServiceRouteTable.Put(serviceRouteKey(service), UsidServiceRouteValue{ - TargetIfindex: 1, Mode: 1, ConsumerToken: currentToken, ProducerToken: currentToken, + TargetIfindex: 1, Mode: 1, ConsumerToken: currentToken, ProducerToken: currentToken, BackendAddr: service.As16(), }); err != nil { t.Fatalf("populate service route: %v", err) } @@ -1892,14 +1894,15 @@ func TestUsidServiceEgress_ReclaimsCollisionFromStaleRemoteGrant(t *testing.T) { SourceAddr: service.As16(), DestAddr: consumer.As16(), } if err := objs.ServiceReverseTable.Put(reverseKey, UsidServiceReverseValue{ - Mode: 2, ProducerToken: currentToken, GrantId: grantID, ReturnSid: [16]byte{1}, + Mode: 2, ProducerToken: currentToken, GrantId: grantID, ReturnSid: [16]byte{1}, FrontendAddr: service.As16(), }); err != nil { t.Fatalf("seed stale remote reverse: %v", err) } if err := objs.ServiceRemoteGrantTable.Put(UsidServiceRemoteGrantKey{ ProducerIfindex: 1, Family: egressRouteFamilyINET6, Protocol: 6, Port: bits.ReverseBytes16(8443), GrantId: grantID, Addr: service.As16(), - }, UsidServiceRemoteGrantValue{ProducerToken: currentToken, ConsumerSid: [16]byte{2}}); err != nil { + }, UsidServiceRemoteGrantValue{ProducerToken: currentToken, ConsumerSid: [16]byte{2}, + FrontendAddr: service.As16()}); err != nil { t.Fatalf("seed stale remote grant: %v", err) } @@ -1924,7 +1927,7 @@ func TestUsidServiceEgress_ReclaimsCollisionFromStaleLocalRoute(t *testing.T) { service := netip.MustParseAddr("fd20:70::100") consumer := netip.MustParseAddr("fd20:70::2") if err := objs.ServiceRouteTable.Put(serviceRouteKey(service), UsidServiceRouteValue{ - TargetIfindex: 1, Mode: 1, ConsumerToken: currentToken, ProducerToken: currentToken, + TargetIfindex: 1, Mode: 1, ConsumerToken: currentToken, ProducerToken: currentToken, BackendAddr: service.As16(), }); err != nil { t.Fatal(err) } @@ -1940,7 +1943,7 @@ func TestUsidServiceEgress_ReclaimsCollisionFromStaleLocalRoute(t *testing.T) { SourceAddr: service.As16(), DestAddr: consumer.As16(), } if err := objs.ServiceReverseTable.Put(reverseKey, UsidServiceReverseValue{ - ConsumerIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1, + ConsumerIfindex: 1, Mode: 1, ConsumerToken: 1, ProducerToken: 1, FrontendAddr: service.As16(), }); err != nil { t.Fatal(err) } diff --git a/internal/plumbing/ebpf/serviceroutemap/lease_clock_linux.go b/internal/plumbing/ebpf/serviceroutemap/lease_clock_linux.go new file mode 100644 index 00000000..5ca539f9 --- /dev/null +++ b/internal/plumbing/ebpf/serviceroutemap/lease_clock_linux.go @@ -0,0 +1,23 @@ +//go:build linux + +// Copyright 2026 Datum Cloud, Inc. +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package serviceroutemap + +import ( + "time" + + "golang.org/x/sys/unix" +) + +func authorizationClock() (time.Time, uint64, error) { + var clock unix.Timespec + // Boottime includes suspension so a suspended node cannot extend a lease. + // Sampling boottime before wall time errs toward shortening the lease. + if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &clock); err != nil { + return time.Time{}, 0, err + } + return time.Now(), uint64(clock.Nano()), nil +} diff --git a/internal/plumbing/ebpf/serviceroutemap/lease_clock_other.go b/internal/plumbing/ebpf/serviceroutemap/lease_clock_other.go new file mode 100644 index 00000000..5e1b9aaa --- /dev/null +++ b/internal/plumbing/ebpf/serviceroutemap/lease_clock_other.go @@ -0,0 +1,16 @@ +//go:build !linux + +// Copyright 2026 Datum Cloud, Inc. +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package serviceroutemap + +import ( + "errors" + "time" +) + +func authorizationClock() (time.Time, uint64, error) { + return time.Time{}, 0, errors.New("authorization leases require Linux") +} diff --git a/internal/plumbing/ebpf/serviceroutemap/table.go b/internal/plumbing/ebpf/serviceroutemap/table.go index 3178d801..a9d72916 100644 --- a/internal/plumbing/ebpf/serviceroutemap/table.go +++ b/internal/plumbing/ebpf/serviceroutemap/table.go @@ -12,6 +12,7 @@ import ( "math/bits" "net" "path/filepath" + "time" "github.com/cilium/ebpf" @@ -38,13 +39,16 @@ const ( ) type serviceRouteValue struct { - TargetIfindex uint32 - Mode uint8 - Pad [3]uint8 - ConsumerToken uint64 - ProducerToken uint64 - GrantID [16]uint8 - TargetSID [16]uint8 + TargetIfindex uint32 + Mode uint8 + Pad [3]uint8 + ConsumerToken uint64 + ProducerToken uint64 + GrantID [16]uint8 + TargetSID [16]uint8 + BackendAddress [16]uint8 + AuthorizationDeadlineNS uint64 + AuthorizationValidUntilNS int64 } const ( @@ -111,6 +115,7 @@ type serviceReverseValue struct { ProducerToken uint64 GrantID [16]uint8 ReturnSID [16]uint8 + FrontendAddress [16]uint8 LastSeenNS uint64 } @@ -124,12 +129,22 @@ type serviceRemoteGrantKey struct { } type serviceRemoteGrantValue struct { - ConsumerSID [16]uint8 - ProducerToken uint64 + ConsumerSID [16]uint8 + FrontendAddress [16]uint8 + ProducerToken uint64 + AuthorizationDeadlineNS uint64 + AuthorizationValidUntilNS int64 } type servicePolicyStateValue struct{ Enabled uint8 } +func addressBytes(address net.IP) []byte { + if ipv4 := address.To4(); ipv4 != nil { + return ipv4 + } + return address.To16() +} + // RouteIdentity describes a forwarding key that must survive a complete // desired-state sweep. type RouteIdentity struct { @@ -181,6 +196,7 @@ type Tables struct { identities usidmap.Table policyState usidmap.Table deny usidmap.Table + leaseClockFn func() (time.Time, uint64, error) } // OpenPinned opens both service maps under pinDir. The caller owns the @@ -509,11 +525,12 @@ func (t *Tables) SweepPolicy(snapshot PolicySnapshot) error { // RegisterRoute installs or replaces a service forwarding entry. func (t *Tables) RegisterRoute( ingressIfindex uint32, - address net.IP, + address, backendAddress net.IP, protocol uint8, port uint16, targetIfindex uint32, consumerToken, producerToken uint64, + authorization ...time.Time, ) error { key, err := routeKey(ingressIfindex, address, protocol, port) if err != nil { @@ -525,6 +542,10 @@ func (t *Tables) RegisterRoute( ConsumerToken: consumerToken, ProducerToken: producerToken, } + copy(value.BackendAddress[:], addressBytes(backendAddress)) + if err := t.authorizeRoute(key, &value, authorization); err != nil { + return err + } if err := t.routes.Put(key, value); err != nil { return fmt.Errorf("serviceroutemap: register route ifindex=%d address=%s: %w", ingressIfindex, address, err) } @@ -533,8 +554,8 @@ func (t *Tables) RegisterRoute( // RegisterRemoteRoute installs an authenticated service tunnel target while // keeping the original inner source address intact. -func (t *Tables) RegisterRemoteRoute(ingressIfindex uint32, address net.IP, grantID [16]byte, - protocol uint8, port uint16, targetSID net.IP, consumerToken uint64) error { +func (t *Tables) RegisterRemoteRoute(ingressIfindex uint32, address, backendAddress net.IP, grantID [16]byte, + protocol uint8, port uint16, targetSID net.IP, consumerToken uint64, authorization ...time.Time) error { key, err := routeKey(ingressIfindex, address, protocol, port) if err != nil { return err @@ -545,14 +566,20 @@ func (t *Tables) RegisterRemoteRoute(ingressIfindex uint32, address net.IP, gran } value := serviceRouteValue{Mode: routeModeRemote, GrantID: grantID, ConsumerToken: consumerToken} copy(value.TargetSID[:], sid) + copy(value.BackendAddress[:], addressBytes(backendAddress)) + if err := t.authorizeRoute(key, &value, authorization); err != nil { + return err + } if err := t.routes.Put(key, value); err != nil { return fmt.Errorf("serviceroutemap: register remote route: %w", err) } return nil } -func (t *Tables) RegisterRemoteGrant(producerIfindex uint32, address net.IP, protocol uint8, port uint16, - grantID [16]byte, consumerSID net.IP, producerToken uint64) error { +func (t *Tables) RegisterRemoteGrant( + producerIfindex uint32, address, frontendAddress net.IP, protocol uint8, port uint16, + grantID [16]byte, consumerSID net.IP, producerToken uint64, authorization ...time.Time, +) error { if t.remoteGrants == nil { return errors.New("serviceroutemap: remote grant table unavailable") } @@ -566,6 +593,10 @@ func (t *Tables) RegisterRemoteGrant(producerIfindex uint32, address net.IP, pro } value := serviceRemoteGrantValue{ProducerToken: producerToken} copy(value.ConsumerSID[:], sid) + copy(value.FrontendAddress[:], addressBytes(frontendAddress)) + if err := t.authorizeGrant(key, &value, authorization); err != nil { + return err + } if err := t.remoteGrants.Put(key, value); err != nil { return fmt.Errorf("serviceroutemap: register remote grant: %w", err) } @@ -718,3 +749,76 @@ func remoteGrantKey( copy(key.Address[:], ipv6) return key, nil } + +// authorizationDeadline converts an absolute lease to the kernel monotonic clock. +// Replaying the same lease retains the earlier pinned deadline even if wall time +// moved backwards. A changed validUntil is an explicit authorization renewal. +func (t *Tables) authorizationDeadline( + authorization []time.Time, previousUntil int64, previousDeadline uint64, +) (int64, uint64, error) { + if len(authorization) == 0 || authorization[0].IsZero() { + return 0, 0, nil + } + clock := t.leaseClockFn + if clock == nil { + clock = authorizationClock + } + now, monotonic, err := clock() + if err != nil { + return 0, 0, fmt.Errorf("serviceroutemap: read authorization clock: %w", err) + } + until := authorization[0] + remaining := until.Sub(now) + if remaining <= 0 { + return 0, 0, errors.New("serviceroutemap: authorization expired") + } + deadline := monotonic + uint64(remaining) + if deadline < monotonic { + return 0, 0, errors.New("serviceroutemap: authorization deadline overflow") + } + if until.UnixNano() == previousUntil && previousDeadline != 0 && previousDeadline < deadline { + deadline = previousDeadline + } + if deadline <= monotonic { + return 0, 0, errors.New("serviceroutemap: pinned authorization expired") + } + return until.UnixNano(), deadline, nil +} + +func (t *Tables) authorizeRoute(key serviceRouteKey, value *serviceRouteValue, authorization []time.Time) error { + if len(authorization) == 0 || authorization[0].IsZero() { + return nil + } + var old serviceRouteValue + if err := t.routes.Lookup(key, &old); err != nil && !errors.Is(err, ebpf.ErrKeyNotExist) { + return fmt.Errorf("serviceroutemap: lookup route authorization: %w", err) + } + until, deadline, err := t.authorizationDeadline( + authorization, old.AuthorizationValidUntilNS, old.AuthorizationDeadlineNS, + ) + if err != nil { + return err + } + value.AuthorizationValidUntilNS, value.AuthorizationDeadlineNS = until, deadline + return nil +} + +func (t *Tables) authorizeGrant( + key serviceRemoteGrantKey, value *serviceRemoteGrantValue, authorization []time.Time, +) error { + if len(authorization) == 0 || authorization[0].IsZero() { + return nil + } + var old serviceRemoteGrantValue + if err := t.remoteGrants.Lookup(key, &old); err != nil && !errors.Is(err, ebpf.ErrKeyNotExist) { + return fmt.Errorf("serviceroutemap: lookup grant authorization: %w", err) + } + until, deadline, err := t.authorizationDeadline( + authorization, old.AuthorizationValidUntilNS, old.AuthorizationDeadlineNS, + ) + if err != nil { + return err + } + value.AuthorizationValidUntilNS, value.AuthorizationDeadlineNS = until, deadline + return nil +} diff --git a/internal/plumbing/ebpf/serviceroutemap/table_test.go b/internal/plumbing/ebpf/serviceroutemap/table_test.go index 520c38ef..ab56518a 100644 --- a/internal/plumbing/ebpf/serviceroutemap/table_test.go +++ b/internal/plumbing/ebpf/serviceroutemap/table_test.go @@ -11,6 +11,7 @@ import ( "net" "reflect" "testing" + "time" "github.com/cilium/ebpf" @@ -119,7 +120,7 @@ func TestTables_RegisterIPv6Route(t *testing.T) { reverse := &fakeTable{} tables := New(routes, access, reverse) address := net.ParseIP("fd20:0:19::53") - if err := tables.RegisterRoute(42, address, ProtocolUDP, 53, 101, 420, 1010); err != nil { + if err := tables.RegisterRoute(42, address, address, ProtocolUDP, 53, 101, 420, 1010); err != nil { t.Fatal(err) } key, ok := routes.putKey.(serviceRouteKey) @@ -141,7 +142,7 @@ func TestTables_RegisterRemoteRoute(t *testing.T) { routes := &fakeTable{} tables := New(routes, &fakeTable{}, &fakeTable{}, &fakeTable{}) grant := [16]byte{1, 2, 3} - if err := tables.RegisterRemoteRoute(42, net.ParseIP("10.0.0.53"), grant, + if err := tables.RegisterRemoteRoute(42, net.ParseIP("10.0.0.53"), net.ParseIP("10.0.0.53"), grant, ProtocolTCP, 443, net.ParseIP("fd00::1234"), 420); err != nil { t.Fatal(err) } @@ -157,7 +158,7 @@ func TestTables_RegisterRemoteGrant(t *testing.T) { tables := New(&fakeTable{}, &fakeTable{}, &fakeTable{}, grants) grantID := [16]byte{9, 8, 7} consumerSID := net.ParseIP("fd00::42") - if err := tables.RegisterRemoteGrant(101, net.ParseIP("fd20::53"), ProtocolTCP, 443, + if err := tables.RegisterRemoteGrant(101, net.ParseIP("fd20::53"), net.ParseIP("fd20::53"), ProtocolTCP, 443, grantID, consumerSID, 1010); err != nil { t.Fatal(err) } @@ -354,3 +355,70 @@ func TestTablesSweepPolicyDeleteFailureIsRetryable(t *testing.T) { t.Fatal("reverse state was not preserved across retry") } } + +func TestAuthorizationLeasePinnedReplayAndRenewal(t *testing.T) { + wall := time.Unix(1800000000, 0) + monotonic := uint64(10000000000) + until := wall.Add(10 * time.Second) + tests := []struct { + name string + remote bool + }{{"LocalRoute", false}, {"RemoteGrant", true}} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + routes, grants := &memoryTable{}, &memoryTable{} + tables := New(routes, &memoryTable{}, &memoryTable{}, grants) + clockWall, clockMono := wall, monotonic + tables.leaseClockFn = func() (time.Time, uint64, error) { return clockWall, clockMono, nil } + address := net.ParseIP("fd20:70::53") + register := func(expiry time.Time) error { + if tt.remote { + return tables.RegisterRemoteGrant( + 1, address, address, ProtocolTCP, 53, [16]byte{1}, net.ParseIP("fd00:1::1"), 1, expiry, + ) + } + return tables.RegisterRoute(1, address, address, ProtocolTCP, 53, 2, 1, 2, expiry) + } + deadline := func() uint64 { + if tt.remote { + for _, v := range grants.entries { + return v.(serviceRemoteGrantValue).AuthorizationDeadlineNS + } + } + for _, v := range routes.entries { + return v.(serviceRouteValue).AuthorizationDeadlineNS + } + return 0 + } + if err := register(until); err != nil { + t.Fatal(err) + } + want := monotonic + uint64(10*time.Second) + if deadline() != want { + t.Fatalf("deadline=%d, want %d", deadline(), want) + } + // A restarted controller with a backward wall clock must not renew the lease. + clockWall, clockMono = wall.Add(-time.Second), monotonic+uint64(time.Second) + if err := register(until); err != nil { + t.Fatal(err) + } + if deadline() != want { + t.Fatalf("replay extended deadline to %d, want %d", deadline(), want) + } + // Expiry remains enforced by the pinned monotonic deadline, even under clock skew. + clockMono = want + if err := register(until); err == nil { + t.Fatal("accepted expired pinned authorization") + } + if err := register(until.Add(10 * time.Second)); err != nil { + t.Fatal(err) + } + if deadline() <= want { + t.Fatal("explicit renewal did not advance deadline") + } + if err := register(clockWall.Add(-time.Second)); err == nil { + t.Fatal("accepted expired absolute authorization") + } + }) + } +} diff --git a/internal/serviceroute/dataplane.go b/internal/serviceroute/dataplane.go index f22acb04..0e974152 100644 --- a/internal/serviceroute/dataplane.go +++ b/internal/serviceroute/dataplane.go @@ -15,6 +15,7 @@ import ( "sort" "strings" "sync" + "time" "github.com/cilium/ebpf" "github.com/vishvananda/netlink" @@ -25,16 +26,18 @@ import ( ) type routeRef struct { - ingressIfindex uint32 - address string - targetIfindex uint32 - mode RouteIntentKind - protocol uint8 - port uint16 - grantID ServiceGrantID - targetSID [16]byte - consumerToken uint64 - producerToken uint64 + ingressIfindex uint32 + address string + backendAddress string + targetIfindex uint32 + mode RouteIntentKind + protocol uint8 + port uint16 + grantID ServiceGrantID + targetSID [16]byte + consumerToken uint64 + producerToken uint64 + authorizationValidUntil int64 } type accessRef struct { @@ -83,13 +86,15 @@ type appliedEntry struct { } type grantRef struct { - producerIfindex uint32 - address string - protocol uint8 - port uint16 - grantID ServiceGrantID - consumerSID [16]byte - producerToken uint64 + producerIfindex uint32 + address string + frontendAddress string + protocol uint8 + port uint16 + grantID ServiceGrantID + consumerSID [16]byte + producerToken uint64 + authorizationValidUntil int64 } type routeState struct { @@ -103,22 +108,23 @@ type routeState struct { type EBPFRouteProgrammer struct { PinDir string - mu sync.Mutex - tables *serviceroutemap.Tables - mapHandles []*ebpf.Map - mapIDs [7]ebpf.MapID - mapIDsValid bool - routeRefs map[string]routeState - accessRefs map[accessKey]accessState - denyRefs map[denyKey]denyState - grantRefs map[grantRef]int - appliedRefs map[string][][]appliedEntry - desiredIntents map[string][]RouteIntent - pendingRollbacks map[string][]appliedEntry - policyFinalized bool - syncFailures int - failedMutations map[string]struct{} - generationIncomplete bool + mu sync.Mutex + tables *serviceroutemap.Tables + mapHandles []*ebpf.Map + mapIDs [7]ebpf.MapID + mapIDsValid bool + routeRefs map[string]routeState + accessRefs map[accessKey]accessState + denyRefs map[denyKey]denyState + grantRefs map[grantRef]int + appliedRefs map[string][][]appliedEntry + desiredIntents map[string][]RouteIntent + pendingRollbacks map[string][]appliedEntry + policyFinalized bool + syncFailures int + failedMutations map[string]struct{} + generationIncomplete bool + generationWasFinalized bool // These hooks keep map replacement and interface recreation tests // deterministic without requiring a privileged kernel eBPF setup. @@ -349,13 +355,28 @@ func (p *EBPFRouteProgrammer) ensureOpen() error { p.desiredIntents = make(map[string][]RouteIntent) return nil } + restorePolicy := p.policyFinalized || (p.generationIncomplete && p.generationWasFinalized) + expired := p.expiredDesiredKeys(time.Now()) + if len(expired) != 0 { + // A replacement map set can still contain pinned rows from an expired + // authorization. Never replay or sweep those rows while serving them. + if err := p.tables.SetPolicyEnabled(false); err != nil { + p.closeHandles() + return err + } + p.pruneDesiredKeys(expired) + } if err := p.rebuildDesired(); err != nil { p.closeHandles() return err } - if p.policyFinalized { - return p.finalizeLocked() + if restorePolicy { + if err := p.finalizeLocked(); err != nil { + return err + } } + p.generationIncomplete = false + p.generationWasFinalized = false return nil } @@ -460,8 +481,12 @@ func (p *EBPFRouteProgrammer) rebuildDesired() error { } func (p *EBPFRouteProgrammer) repairDesired() error { - needsRepair := p.generationIncomplete + expired := p.expiredDesiredKeys(time.Now()) + needsRepair := p.generationIncomplete || len(expired) != 0 for _, key := range sortedIntentKeys(p.desiredIntents) { + if _, isExpired := expired[key]; isExpired { + continue + } intents := p.desiredIntents[key] sets := p.appliedRefs[key] if len(sets) != len(intents) { @@ -489,8 +514,15 @@ func (p *EBPFRouteProgrammer) repairDesired() error { if err := p.tables.SetPolicyEnabled(false); err != nil { return err } + if !p.generationIncomplete { + p.generationWasFinalized = p.policyFinalized + } p.policyFinalized = false p.generationIncomplete = true + // Expired desired state is not an invalid replacement intent. It is stale + // state to remove, including its access grants and reference counts. This + // also lets the caller Remove it or Apply an explicit renewal afterwards. + p.pruneDesiredKeys(expired) if err := p.tables.SweepPolicy(serviceroutemap.PolicySnapshot{}); err != nil { return fmt.Errorf("clear stale service policy generation: %w", err) } @@ -506,14 +538,36 @@ func (p *EBPFRouteProgrammer) repairDesired() error { p.markSyncFailure("__generation__") return err } - if err := p.finalizeLocked(); err != nil { - p.markSyncFailure("__generation__") - return err + if p.generationWasFinalized { + if err := p.finalizeLocked(); err != nil { + p.markSyncFailure("__generation__") + return err + } } p.generationIncomplete = false + p.generationWasFinalized = false return nil } +func (p *EBPFRouteProgrammer) expiredDesiredKeys(now time.Time) map[string]struct{} { + expired := make(map[string]struct{}) + for key, intents := range p.desiredIntents { + for _, intent := range intents { + if !intent.AuthorizationValidUntil.IsZero() && !now.Before(intent.AuthorizationValidUntil) { + expired[key] = struct{}{} + break + } + } + } + return expired +} + +func (p *EBPFRouteProgrammer) pruneDesiredKeys(keys map[string]struct{}) { + for key := range keys { + delete(p.desiredIntents, key) + } +} + func (p *EBPFRouteProgrammer) markSyncFailure(key string) { if p.failedMutations == nil { p.failedMutations = make(map[string]struct{}) @@ -581,7 +635,14 @@ func (p *EBPFRouteProgrammer) retryRollback(key string) error { } func (p *EBPFRouteProgrammer) entries(intent RouteIntent) ([]appliedEntry, error) { + if !intent.AuthorizationValidUntil.IsZero() && !time.Now().Before(intent.AuthorizationValidUntil) { + return nil, errors.New("service route authorization expired") + } serviceIP := intent.Service.IP + frontendIP := serviceIP + if intent.Frontend != nil { + frontendIP = intent.Frontend.IP + } entries := make([]appliedEntry, 0, 2*len(intent.Ports)+2) if intent.Kind == RouteIntentRemoteProducer { serviceIfindex, err := p.target(intent.ServiceDevice) @@ -611,9 +672,10 @@ func (p *EBPFRouteProgrammer) entries(intent RouteIntent) ([]appliedEntry, error if err != nil { return nil, err } - ref := grantRef{producerIfindex: serviceIfindex, address: serviceIP.String(), protocol: protocol, + ref := grantRef{producerIfindex: serviceIfindex, address: serviceIP.String(), + frontendAddress: frontendIP.String(), protocol: protocol, port: uint16(port.Port), grantID: intent.GrantID, consumerSID: consumerSID, - producerToken: producerToken} + producerToken: producerToken, authorizationValidUntil: authorizationUnixNano(intent.AuthorizationValidUntil)} entries = append(entries, appliedEntry{grant: &ref}) } return entries, nil @@ -629,11 +691,11 @@ func (p *EBPFRouteProgrammer) entries(intent RouteIntent) ([]appliedEntry, error // A protocol/port-zero marker makes malformed or non-TCP/UDP traffic to a // configured service fail closed without conflating distinct real tuples. entries = append(entries, appliedEntry{access: &accessRef{ - ingressIfindex: consumerIfindex, address: serviceIP.String(), attachmentToken: consumerToken, + ingressIfindex: consumerIfindex, address: frontendIP.String(), attachmentToken: consumerToken, markerDirections: serviceroutemap.MarkerRequest, }}) entries = append(entries, appliedEntry{deny: &denyRef{ - ingressIfindex: consumerIfindex, address: serviceIP.String(), attachmentToken: consumerToken, + ingressIfindex: consumerIfindex, address: frontendIP.String(), attachmentToken: consumerToken, markerDirections: serviceroutemap.MarkerRequest, }}) var targetIfindex uint32 @@ -671,13 +733,14 @@ func (p *EBPFRouteProgrammer) entries(intent RouteIntent) ([]appliedEntry, error return nil, err } entries = append(entries, appliedEntry{access: &accessRef{ - ingressIfindex: consumerIfindex, address: serviceIP.String(), protocol: protocol, port: uint16(port.Port), + ingressIfindex: consumerIfindex, address: frontendIP.String(), protocol: protocol, port: uint16(port.Port), attachmentToken: consumerToken, }}) - ref := routeRef{ingressIfindex: consumerIfindex, address: serviceIP.String(), mode: intent.Kind, + ref := routeRef{ingressIfindex: consumerIfindex, address: frontendIP.String(), + backendAddress: serviceIP.String(), mode: intent.Kind, protocol: protocol, port: uint16(port.Port), grantID: intent.GrantID, targetIfindex: targetIfindex, targetSID: targetSID, consumerToken: consumerToken, - producerToken: producerToken} + producerToken: producerToken, authorizationValidUntil: authorizationUnixNano(intent.AuthorizationValidUntil)} entries = append(entries, appliedEntry{route: &ref}) } return entries, nil @@ -707,8 +770,9 @@ func (p *EBPFRouteProgrammer) acquire(entry appliedEntry) error { if entry.grant != nil { if p.grantRefs[*entry.grant] == 0 { g := entry.grant - if err := p.tables.RegisterRemoteGrant(g.producerIfindex, net.ParseIP(g.address), g.protocol, g.port, - [16]byte(g.grantID), net.IP(g.consumerSID[:]), g.producerToken); err != nil { + if err := p.tables.RegisterRemoteGrant(g.producerIfindex, net.ParseIP(g.address), + net.ParseIP(g.frontendAddress), g.protocol, g.port, [16]byte(g.grantID), net.IP(g.consumerSID[:]), + g.producerToken, authorizationTime(g.authorizationValidUntil)); err != nil { return err } } @@ -759,12 +823,14 @@ func (p *EBPFRouteProgrammer) acquire(entry appliedEntry) error { var err error if entry.route.mode == RouteIntentRemoteConsumer { err = p.tables.RegisterRemoteRoute(entry.route.ingressIfindex, net.ParseIP(entry.route.address), + net.ParseIP(entry.route.backendAddress), [16]byte(entry.route.grantID), entry.route.protocol, entry.route.port, net.IP(entry.route.targetSID[:]), - entry.route.consumerToken) + entry.route.consumerToken, authorizationTime(entry.route.authorizationValidUntil)) } else { err = p.tables.RegisterRoute(entry.route.ingressIfindex, net.ParseIP(entry.route.address), + net.ParseIP(entry.route.backendAddress), entry.route.protocol, entry.route.port, entry.route.targetIfindex, - entry.route.consumerToken, entry.route.producerToken) + entry.route.consumerToken, entry.route.producerToken, authorizationTime(entry.route.authorizationValidUntil)) } if err != nil { return err @@ -945,9 +1011,10 @@ func routeRefKey(ref routeRef) string { func intentKey(intent RouteIntent) string { var builder strings.Builder - fmt.Fprintf(&builder, "%s|%s|%s|%s|%s|%s|%s|%s|%x", - intent.Attachment, intent.ProducerAttachment, intent.Kind, intent.Service, intent.ConsumerDevice, + fmt.Fprintf(&builder, "%s|%s|%s|%s|%s|%s|%s|%s|%s|%x", + intent.Attachment, intent.ProducerAttachment, intent.Kind, intent.Service, intent.Frontend, intent.ConsumerDevice, intent.ServiceDevice, intent.ServiceSID, intent.ConsumerSID, intent.GrantID) + fmt.Fprintf(&builder, "|lease:%d", authorizationUnixNano(intent.AuthorizationValidUntil)) for _, port := range intent.Ports { fmt.Fprintf(&builder, "|%s:%d", port.Protocol, port.Port) } @@ -962,6 +1029,10 @@ func cloneRouteIntent(intent RouteIntent) RouteIntent { Mask: append(net.IPMask(nil), intent.Service.Mask...), } } + if intent.Frontend != nil { + cloned.Frontend = &net.IPNet{IP: append(net.IP(nil), intent.Frontend.IP...), + Mask: append(net.IPMask(nil), intent.Frontend.Mask...)} + } cloned.ServiceSID = append(net.IP(nil), intent.ServiceSID...) cloned.ConsumerSID = append(net.IP(nil), intent.ConsumerSID...) cloned.Ports = append([]api.ServiceRouteProtocolPort(nil), intent.Ports...) @@ -974,3 +1045,17 @@ func (p *EBPFRouteProgrammer) pinDir() string { } return attach.PinDir } + +func authorizationUnixNano(until time.Time) int64 { + if until.IsZero() { + return 0 + } + return until.UnixNano() +} + +func authorizationTime(until int64) time.Time { + if until == 0 { + return time.Time{} + } + return time.Unix(0, until) +} diff --git a/internal/serviceroute/dataplane_test.go b/internal/serviceroute/dataplane_test.go index 5afc747b..f4642255 100644 --- a/internal/serviceroute/dataplane_test.go +++ b/internal/serviceroute/dataplane_test.go @@ -11,7 +11,9 @@ import ( "fmt" "net" "reflect" + "strconv" "testing" + "time" "github.com/cilium/ebpf" @@ -33,6 +35,7 @@ const ( testRemoteServiceAddress = "10.0.0.53" testConsumerDeviceName = "consumer0" testProducerDeviceName = "producer0" + testOtherConsumerDevice = "consumer-b" ) func (t *recordingTable) Put(_ any, value any) error { @@ -599,3 +602,243 @@ func TestEntriesIncludeConsumerAndProducerFragmentMarkers(t *testing.T) { t.Fatalf("remote producer entries = %#v, want access marker, deny marker, then grant", remoteEntries) } } + +func TestApplyRejectsExpiredAuthorization(t *testing.T) { + routes, access, grants := &recordingTable{}, &recordingTable{}, &recordingTable{} + programmer := testRouteProgrammer(routes, access, grants) + intent := localTestIntent() + intent.AuthorizationValidUntil = time.Now().Add(-time.Second) + if err := programmer.Apply(intent); err == nil { + t.Fatal("accepted expired authorization") + } + if routes.puts != 0 || access.puts != 0 || grants.puts != 0 { + t.Fatal("expired authorization changed forwarding maps") + } + renewed := intent + renewed.AuthorizationValidUntil = time.Now().Add(time.Minute) + if intentKey(intent) == intentKey(renewed) { + t.Fatal("authorization renewal retained old intent identity") + } +} + +// Unlike recordingTable, this table keeps actual keys so generation sweeps can +// prove that expired grants disappear while unrelated tenants retain theirs. +type persistedPolicyTable struct { + rows map[any]any + putErr error +} + +func (table *persistedPolicyTable) Put(key, value any) error { + if table.putErr != nil { + return table.putErr + } + if table.rows == nil { + table.rows = make(map[any]any) + } + table.rows[key] = value + return nil +} + +func (table *persistedPolicyTable) Delete(key any) error { + delete(table.rows, key) + return nil +} + +func (table *persistedPolicyTable) Lookup(key, value any) error { + stored, ok := table.rows[key] + if !ok { + return ebpf.ErrKeyNotExist + } + reflect.ValueOf(value).Elem().Set(reflect.ValueOf(stored)) + return nil +} + +func (table *persistedPolicyTable) Iterate() usidmap.Iterator { + iterator := &persistedPolicyIterator{} + for key, value := range table.rows { + iterator.entries = append(iterator.entries, [2]any{key, value}) + } + return iterator +} + +type persistedPolicyIterator struct{ entries [][2]any } + +func (iterator *persistedPolicyIterator) Next(key, value any) bool { + if len(iterator.entries) == 0 { + return false + } + entry := iterator.entries[0] + iterator.entries = iterator.entries[1:] + reflect.ValueOf(key).Elem().Set(reflect.ValueOf(entry[0])) + reflect.ValueOf(value).Elem().Set(reflect.ValueOf(entry[1])) + return true +} + +func (*persistedPolicyIterator) Err() error { return nil } + +func leasedTestProgrammer() (*EBPFRouteProgrammer, *persistedPolicyTable, *persistedPolicyTable, + *persistedPolicyTable) { + routes, grants, gate := &persistedPolicyTable{}, &persistedPolicyTable{}, &persistedPolicyTable{} + programmer := testRouteProgrammer(&recordingTable{}, &recordingTable{}, &recordingTable{}) + programmer.tables = serviceroutemap.New(routes, &persistedPolicyTable{}, &persistedPolicyTable{}, grants, + &recordingTable{identityTokens: map[uint32]uint64{10: 100, 20: 200, 42: 420}}, gate, &persistedPolicyTable{}) + programmer.mapIDs = [7]ebpf.MapID{1, 2, 3, 4, 5, 6, 7} + programmer.mapIDsValid = true + programmer.readMapIDsFn = func(string) ([7]ebpf.MapID, error) { return programmer.mapIDs, nil } + programmer.targetIndexFn = func(name string) (uint32, error) { + switch name { + case testConsumerDeviceName: + return 10, nil + case testProducerDeviceName: + return 20, nil + case testOtherConsumerDevice: + return 42, nil + default: + return 0, fmt.Errorf("unknown interface %q", name) + } + } + return programmer, routes, grants, gate +} + +func waitForIntentExpiry(intent RouteIntent) { + time.Sleep(time.Until(intent.AuthorizationValidUntil) + time.Millisecond) +} + +func policyGateEnabled(table *persistedPolicyTable) bool { + value, ok := table.rows[uint32(0)] + return ok && reflect.ValueOf(value).FieldByName("Enabled").Uint() != 0 +} + +func TestExpiredAuthorizationRemovalAndRenewalPreservesUnrelatedTenant(t *testing.T) { + for _, kind := range []RouteIntentKind{RouteIntentLocal, RouteIntentRemoteConsumer, RouteIntentRemoteProducer} { + t.Run(string(kind), func(t *testing.T) { + programmer, routes, grants, gate := leasedTestProgrammer() + intent := localTestIntent() + intent.Kind, intent.GrantID = kind, ServiceGrantID{1} + intent.ServiceSID, intent.ConsumerSID = net.ParseIP("fd00::1"), net.ParseIP("fd00::2") + intent.AuthorizationValidUntil = time.Now().Add(time.Second) + unrelated := localTestIntent() + unrelated.ConsumerDevice = testOtherConsumerDevice + if err := programmer.Apply(intent); err != nil { + t.Fatal(err) + } + if err := programmer.Apply(unrelated); err != nil { + t.Fatal(err) + } + if err := programmer.Finalize(); err != nil { + t.Fatal(err) + } + unrelatedEntries := programmer.appliedRefs[intentKey(unrelated)] + waitForIntentExpiry(intent) + if err := programmer.Remove(intent); err != nil { + t.Fatalf("Remove expired authorization: %v", err) + } + if len(routes.rows) != 1 || len(grants.rows) != 0 { + t.Fatalf("expired state remains: routes=%d grants=%d", len(routes.rows), len(grants.rows)) + } + if len(programmer.desiredIntents) != 1 || len(programmer.appliedRefs) != 1 || + !reflect.DeepEqual(programmer.appliedRefs[intentKey(unrelated)], unrelatedEntries) { + t.Fatal("expiry cleanup changed the unrelated tenant or retained expired desired state") + } + if !policyGateEnabled(gate) { + t.Fatal("expiry cleanup did not restore the unrelated tenant's finalized generation") + } + renewed := intent + renewed.AuthorizationValidUntil = time.Now().Add(time.Minute) + if err := programmer.Apply(renewed); err != nil { + t.Fatalf("Apply explicit renewal without restart: %v", err) + } + if err := programmer.Remove(intent); err != nil { + t.Fatalf("repeat old removal: %v", err) + } + if len(programmer.appliedRefs[intentKey(renewed)]) != 1 || len(programmer.desiredIntents) != 2 { + t.Fatal("old removal removed the explicit renewal") + } + }) + } +} + +func TestExpiredGenerationRetryKeepsPolicyClosedUntilValidStateRestored(t *testing.T) { + programmer, routes, _, gate := leasedTestProgrammer() + intent := localTestIntent() + intent.AuthorizationValidUntil = time.Now().Add(time.Second) + unrelated := localTestIntent() + unrelated.ConsumerDevice = testOtherConsumerDevice + for _, desired := range []RouteIntent{intent, unrelated} { + if err := programmer.Apply(desired); err != nil { + t.Fatal(err) + } + } + if err := programmer.Finalize(); err != nil { + t.Fatal(err) + } + waitForIntentExpiry(intent) + routes.putErr = errors.New("transient unrelated route rebuild failure") + if err := programmer.Remove(intent); err == nil { + t.Fatal("expired removal succeeded despite a failed generation rebuild") + } + if policyGateEnabled(gate) || !programmer.generationIncomplete { + t.Fatal("partial expiry cleanup reopened the policy gate") + } + routes.putErr = nil + if err := programmer.Remove(intent); err != nil { + t.Fatalf("retry expired removal: %v", err) + } + if !policyGateEnabled(gate) || programmer.generationIncomplete || len(routes.rows) != 1 { + t.Fatal("retry did not restore the valid unrelated generation") + } +} + +func TestMapReplayPrunesExpiredAuthorizationAndPreservesStartupGate(t *testing.T) { + for _, finalized := range []bool{false, true} { + t.Run(strconv.FormatBool(finalized), func(t *testing.T) { + programmer, oldRoutes, _, _ := leasedTestProgrammer() + intent := localTestIntent() + intent.AuthorizationValidUntil = time.Now().Add(time.Second) + unrelated := localTestIntent() + unrelated.ConsumerDevice = testOtherConsumerDevice + for _, desired := range []RouteIntent{intent, unrelated} { + if err := programmer.Apply(desired); err != nil { + t.Fatal(err) + } + } + if finalized { + if err := programmer.Finalize(); err != nil { + t.Fatal(err) + } + } + waitForIntentExpiry(intent) + newRoutes, newGate := &persistedPolicyTable{}, &persistedPolicyTable{} + // Simulate a replacement map set retaining both pinned route rows. + for key, value := range oldRoutes.rows { + if err := newRoutes.Put(key, value); err != nil { + t.Fatal(err) + } + } + newIDs := [7]ebpf.MapID{11, 12, 13, 14, 15, 16, 17} + programmer.readMapIDsFn = func(string) ([7]ebpf.MapID, error) { return newIDs, nil } + programmer.openMapSetFn = func(string) (*serviceroutemap.Tables, []*ebpf.Map, [7]ebpf.MapID, error) { + return serviceroutemap.New(newRoutes, &persistedPolicyTable{}, &persistedPolicyTable{}, + &persistedPolicyTable{}, &recordingTable{identityTokens: map[uint32]uint64{10: 100, 20: 200, 42: 420}}, + newGate, &persistedPolicyTable{}), nil, newIDs, nil + } + if err := programmer.Initialize(); err != nil { + t.Fatalf("replay after expiry: %v", err) + } + if len(programmer.desiredIntents) != 1 { + t.Fatal("replay retained expired desired authorization or lost the unrelated tenant") + } + if policyGateEnabled(newGate) != finalized { + t.Fatalf("replay gate enabled=%t, want prior finalized state %t", policyGateEnabled(newGate), finalized) + } + if !finalized { + if err := programmer.Finalize(); err != nil { + t.Fatalf("finalize complete startup snapshot: %v", err) + } + } + if len(newRoutes.rows) != 1 || !policyGateEnabled(newGate) { + t.Fatal("finalized replay did not sweep the expired pinned authorization") + } + }) + } +} diff --git a/internal/serviceroute/planner.go b/internal/serviceroute/planner.go index 394f32f6..b3274b58 100644 --- a/internal/serviceroute/planner.go +++ b/internal/serviceroute/planner.go @@ -11,6 +11,7 @@ import ( "fmt" "net" "sort" + "time" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,16 +34,18 @@ type ServiceGrantID [16]byte // RouteIntent is one node's half of an attachment-scoped service path. type RouteIntent struct { - Attachment types.NamespacedName - ProducerAttachment types.NamespacedName - Kind RouteIntentKind - Service *net.IPNet - ConsumerDevice string - ServiceDevice string - ServiceSID net.IP - ConsumerSID net.IP - GrantID ServiceGrantID - Ports []networkv1alpha1.ServiceRouteProtocolPort + Attachment types.NamespacedName + ProducerAttachment types.NamespacedName + Kind RouteIntentKind + Service *net.IPNet + Frontend *net.IPNet + ConsumerDevice string + ServiceDevice string + ServiceSID net.IP + ConsumerSID net.IP + GrantID ServiceGrantID + AuthorizationValidUntil time.Time + Ports []networkv1alpha1.ServiceRouteProtocolPort } type RouteProgrammer interface {