From fb112e77f36ea0e7bacb78369c6b7f73149b3560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D1=83=D0=BA=D0=B0=20=D0=92=D0=B8=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D1=8C=D0=B5=D0=B2=D0=B8?= =?UTF-8?q?=D1=87?= <78735360+VITEK-THE-BEST@users.noreply.github.com> Date: Mon, 14 Apr 2025 02:49:21 +0500 Subject: [PATCH 01/18] fix: always parse TCP header in order to SDF filters work (#606) --- cmd/ebpf/xdp/n3n6_entrypoint.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index dc85a15d..1d528277 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -549,6 +549,7 @@ static __always_inline enum xdp_action handle_ip4(struct packet_context *ctx) { break; case IPPROTO_TCP: increment_counter(ctx->counters, rx_tcp); + parse_tcp(ctx); break; default: increment_counter(ctx->counters, rx_other); @@ -576,6 +577,7 @@ static __always_inline enum xdp_action handle_ip6(struct packet_context *ctx) { break; case IPPROTO_TCP: increment_counter(ctx->counters, rx_tcp); + parse_tcp(ctx); break; default: increment_counter(ctx->counters, rx_other); @@ -648,4 +650,4 @@ int upf_ip_entrypoint_func(struct xdp_md *ctx) { return action; } -char _license[] SEC("license") = "GPL"; \ No newline at end of file +char _license[] SEC("license") = "GPL"; From f2fe18367226248159d0566fa05625ffbb13dc48 Mon Sep 17 00:00:00 2001 From: Alex P Date: Sat, 29 Aug 2026 20:16:12 +0300 Subject: [PATCH 02/18] fix(core): recognize UEIPAddress in UpdatePDR (#591) Adapted from upstream a1b6f6a to the refactored extractPDR structure. Lookup UEIPAddress within PDI (findIEindex) instead of on PDR directly, so that UpdatePDR correctly recognizes UE IP address changes. Also fix a latent bug where the final 'missing' branch returned a nil error. Co-authored-by: Junxiao Shi Co-authored-by: Alex Rozhkevich --- cmd/core/pdr_creation_context.go | 62 ++++++++++++++------------- cmd/core/pdr_creation_context_test.go | 17 ++++++++ 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/cmd/core/pdr_creation_context.go b/cmd/core/pdr_creation_context.go index 8d69f95d..e28843d8 100644 --- a/cmd/core/pdr_creation_context.go +++ b/cmd/core/pdr_creation_context.go @@ -136,40 +136,43 @@ func (pdrContext *PDRCreationContext) extractPDR(pdr *ie.IE, spdrInfo *SPDRInfo) } } - if teidPdiId := findIEindex(pdi, 21); teidPdiId != -1 { // IE Type F-TEID - if fteid, err := pdi[teidPdiId].FTEID(); err == nil { - var teid = fteid.TEID - if fteid.HasCh() { - var allocate = true - if fteid.HasChID() { - if teidFromCache, ok := pdrContext.hasTEIDCache(fteid.ChooseID); ok { - allocate = false - teid = teidFromCache - spdrInfo.Allocated = true - } - } - if allocate { - allocatedTeid, err := pdrContext.getFTEID(pdrContext.Session.RemoteSEID, spdrInfo.PdrID) - if err != nil { - log.Error().Msgf("AllocateTEID err: %v", err) - return fmt.Errorf("can't allocate TEID: %s", causeToString(ie.CauseNoResourcesAvailable)) - } - teid = allocatedTeid + if teidPdiId := findIEindex(pdi, ie.FTEID); teidPdiId != -1 { // IE Type F-TEID + fteid, err := pdi[teidPdiId].FTEID() + if err != nil { + return fmt.Errorf("F-TEID IE is missing") + } + var teid = fteid.TEID + if fteid.HasCh() { + var allocate = true + if fteid.HasChID() { + if teidFromCache, ok := pdrContext.hasTEIDCache(fteid.ChooseID); ok { + allocate = false + teid = teidFromCache spdrInfo.Allocated = true - if fteid.HasChID() { - pdrContext.setTEIDCache(fteid.ChooseID, teid) - } } } - spdrInfo.Teid = teid - } - - if spdrInfo.Teid == 0 { - return fmt.Errorf("F-TEID IE is missing") + if allocate { + allocatedTeid, err := pdrContext.getFTEID(pdrContext.Session.RemoteSEID, spdrInfo.PdrID) + if err != nil { + log.Error().Msgf("AllocateTEID err: %v", err) + return fmt.Errorf("can't allocate TEID: %s", causeToString(ie.CauseNoResourcesAvailable)) + } + teid = allocatedTeid + spdrInfo.Allocated = true + if fteid.HasChID() { + pdrContext.setTEIDCache(fteid.ChooseID, teid) + } + } } + spdrInfo.Teid = teid + return nil } - if ueIP, err := pdr.UEIPAddress(); err == nil { + if ueipPdiId := findIEindex(pdi, ie.UEIPAddress); ueipPdiId != -1 { + ueIP, err := pdi[ueipPdiId].UEIPAddress() + if err != nil { + return fmt.Errorf("UE IP Address IE is missing") + } if config.Conf.FeatureUEIP && hasCHV4(ueIP.Flags) { if ip, err := pdrContext.getIP(); err == nil { ueIP.IPv4Address = cloneIP(ip) @@ -189,11 +192,12 @@ func (pdrContext *PDRCreationContext) extractPDR(pdr *ie.IE, spdrInfo *SPDRInfo) if ueIP.IPv6Address != nil { spdrInfo.Ipv6 = cloneIP(ueIP.IPv6Address) } + return nil } if spdrInfo.Teid == 0 && spdrInfo.Ipv4 == nil && spdrInfo.Ipv6 == nil { log.Warn().Msg("Both F-TEID IE and UE IP Address IE are missing") - return err + return fmt.Errorf("both F-TEID IE and UE IP Address IE are missing") } return nil diff --git a/cmd/core/pdr_creation_context_test.go b/cmd/core/pdr_creation_context_test.go index 6b995da3..78d56c5e 100644 --- a/cmd/core/pdr_creation_context_test.go +++ b/cmd/core/pdr_creation_context_test.go @@ -103,6 +103,23 @@ func TestPDRCreationContext_extractPDR(t *testing.T) { }, wantErr: false, }, + { + name: "UpdatePDR with UEIPAddress", + fields: fields{}, + args: args{ + pdr: ie.NewUpdatePDR( + ie.NewPDRID(2), + ie.NewPrecedence(255), + ie.NewPDI( + ie.NewSourceInterface(ie.SrcInterfaceCore), + ie.NewNetworkInstance("a00"), + ie.NewUEIPAddress(0x06, "10.128.0.1", "", 0, 0), + ), + ), + spdrInfo: &SPDRInfo{}, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 2658b9223088515bfd6085d9661de41be4b00740 Mon Sep 17 00:00:00 2001 From: Alex P Date: Sat, 29 Aug 2026 20:19:39 +0300 Subject: [PATCH 03/18] refactor(xdp): rename GTPU[IPv4+TEID]->PDR map (#593) Rename PdrMapUplinkIp4 to PdrMapTeidIp4 across Go and eBPF code for clarity (the map is keyed by TEID, not by uplink). Adapted from upstream 3dd23b4 to release branch layout (pdr.go in api/rest, stub file, PDR_MAP_SIZE macro unchanged). Author: Pau Espin Pedrol --- cmd/api/rest/pdr.go | 2 +- cmd/ebpf/objects.go | 4 ++-- cmd/ebpf/objects_test.go | 2 +- cmd/ebpf/pdr.go | 10 +++++----- cmd/ebpf/stub_generated_nonlinux.go | 2 +- cmd/ebpf/xdp/n3n6_entrypoint.c | 2 +- cmd/ebpf/xdp/pdr.h | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/api/rest/pdr.go b/cmd/api/rest/pdr.go index ae511e96..2f0b43bc 100644 --- a/cmd/api/rest/pdr.go +++ b/cmd/api/rest/pdr.go @@ -47,7 +47,7 @@ func (h *ApiHandler) getUplinkPdrValue(c *gin.Context) { } var value ebpf.PdrInfo - if err = h.BpfObjects.IpEntrypointObjects.PdrMapUplinkIp4.Lookup(uint32(id), unsafe.Pointer(&value)); err != nil { + if err = h.BpfObjects.IpEntrypointObjects.PdrMapTeidIp4.Lookup(uint32(id), unsafe.Pointer(&value)); err != nil { log.Warn().Err(err).Msgf("can't get pdr from map") c.JSON(http.StatusNotFound, gin.H{"error": "no pdr found"}) return diff --git a/cmd/ebpf/objects.go b/cmd/ebpf/objects.go index b65c1003..9ac77586 100644 --- a/cmd/ebpf/objects.go +++ b/cmd/ebpf/objects.go @@ -86,7 +86,7 @@ func (bpfObjects *BpfObjects) Load() error { "far_map": bpfObjects.farMapSize, "pdr_map_downlink_ip4": bpfObjects.pdrMapSize, "pdr_map_downlink_ip6": bpfObjects.pdrMapSize, - "pdr_map_uplink_ip4": bpfObjects.pdrMapSize, + "pdr_map_teid_ip4": bpfObjects.pdrMapSize, "urr_map": bpfObjects.urrMapSize, } @@ -251,7 +251,7 @@ func (bpfObjects *BpfObjects) ResizeAllMaps(qerMapSize uint32, farMapSize uint32 log.Info().Msgf("Failed to resize PDR map: %s", err) return err } - if err := ResizeEbpfMap(&bpfObjects.PdrMapUplinkIp4, bpfObjects.UpfIpEntrypointFunc, pdrMapSize); err != nil { + if err := ResizeEbpfMap(&bpfObjects.PdrMapTeidIp4, bpfObjects.UpfIpEntrypointFunc, pdrMapSize); err != nil { log.Info().Msgf("Failed to resize PDR map: %s", err) return err } diff --git a/cmd/ebpf/objects_test.go b/cmd/ebpf/objects_test.go index 82b84af6..1bed08c8 100644 --- a/cmd/ebpf/objects_test.go +++ b/cmd/ebpf/objects_test.go @@ -195,7 +195,7 @@ func testGtpWithPDRBenchmark(bpfObjects *BpfObjects, repeat int) (int64, error) if err := bpfObjects.QerMap.Put(uint32(1), unsafe.Pointer(&qer)); err != nil { return 0, fmt.Errorf("benchmark run failed: %v", err) } - if err := bpfObjects.PdrMapUplinkIp4.Put(teid, unsafe.Pointer(&pdr)); err != nil { + if err := bpfObjects.PdrMapTeidIp4.Put(teid, unsafe.Pointer(&pdr)); err != nil { return 0, fmt.Errorf("benchmark run failed: %v", err) } diff --git a/cmd/ebpf/pdr.go b/cmd/ebpf/pdr.go index 11ce1ba8..ee645f5f 100644 --- a/cmd/ebpf/pdr.go +++ b/cmd/ebpf/pdr.go @@ -59,13 +59,13 @@ func (bpfObjects *BpfObjects) PutPdrUplink(teid uint32, pdrInfo PdrInfo) error { var pdrToStore IpEntrypointPdrInfo var err error if pdrInfo.SdfFilter != nil { - if pdrToStore, err = PreprocessPdrWithSdf(bpfObjects.PdrMapUplinkIp4.Lookup, teid, pdrInfo); err != nil { + if pdrToStore, err = PreprocessPdrWithSdf(bpfObjects.PdrMapTeidIp4.Lookup, teid, pdrInfo); err != nil { return err } } else { pdrToStore = ToIpEntrypointPdrInfo(pdrInfo) } - return bpfObjects.PdrMapUplinkIp4.Put(teid, unsafe.Pointer(&pdrToStore)) + return bpfObjects.PdrMapTeidIp4.Put(teid, unsafe.Pointer(&pdrToStore)) } func (bpfObjects *BpfObjects) PutPdrDownlink(ip net.IP, pdrInfo PdrInfo) error { @@ -95,13 +95,13 @@ func (bpfObjects *BpfObjects) UpdatePdrUplink(teid uint32, pdrInfo PdrInfo) erro var pdrToStore IpEntrypointPdrInfo var err error if pdrInfo.SdfFilter != nil { - if pdrToStore, err = PreprocessPdrWithSdf(bpfObjects.PdrMapUplinkIp4.Lookup, teid, pdrInfo); err != nil { + if pdrToStore, err = PreprocessPdrWithSdf(bpfObjects.PdrMapTeidIp4.Lookup, teid, pdrInfo); err != nil { return err } } else { pdrToStore = ToIpEntrypointPdrInfo(pdrInfo) } - return bpfObjects.PdrMapUplinkIp4.Update(teid, unsafe.Pointer(&pdrToStore), ebpf.UpdateExist) + return bpfObjects.PdrMapTeidIp4.Update(teid, unsafe.Pointer(&pdrToStore), ebpf.UpdateExist) } func (bpfObjects *BpfObjects) UpdatePdrDownlink(ipv4 net.IP, pdrInfo PdrInfo) error { @@ -120,7 +120,7 @@ func (bpfObjects *BpfObjects) UpdatePdrDownlink(ipv4 net.IP, pdrInfo PdrInfo) er func (bpfObjects *BpfObjects) DeletePdrUplink(teid uint32) error { log.Debug().Msgf("EBPF: Delete PDR Uplink: teid=%d", teid) - return bpfObjects.PdrMapUplinkIp4.Delete(teid) + return bpfObjects.PdrMapTeidIp4.Delete(teid) } func (bpfObjects *BpfObjects) DeletePdrDownlink(ipv4 net.IP) error { diff --git a/cmd/ebpf/stub_generated_nonlinux.go b/cmd/ebpf/stub_generated_nonlinux.go index b8fe5c94..1b6eedb0 100644 --- a/cmd/ebpf/stub_generated_nonlinux.go +++ b/cmd/ebpf/stub_generated_nonlinux.go @@ -15,7 +15,7 @@ type IpEntrypointObjects struct { UrrMap *ebpf.Map PdrMapDownlinkIp4 *ebpf.Map PdrMapDownlinkIp6 *ebpf.Map - PdrMapUplinkIp4 *ebpf.Map + PdrMapTeidIp4 *ebpf.Map UpfExtStat *ebpf.Map UpfRouteStat *ebpf.Map } diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 1d528277..535b5558 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -353,7 +353,7 @@ static __always_inline enum xdp_action handle_gtp_packet(struct packet_context * * Step 1: search for PDR and apply PDR instructions */ __u32 teid = bpf_htonl(ctx->gtp->teid); - struct pdr_info *session = bpf_map_lookup_elem(&pdr_map_uplink_ip4, &teid); + struct pdr_info *session = bpf_map_lookup_elem(&pdr_map_teid_ip4, &teid); if (!session) { upf_printk("upf: [n3] no session for teid:%u", teid); return DEFAULT_XDP_ACTION; diff --git a/cmd/ebpf/xdp/pdr.h b/cmd/ebpf/xdp/pdr.h index e73dfb2a..89d7a6dd 100644 --- a/cmd/ebpf/xdp/pdr.h +++ b/cmd/ebpf/xdp/pdr.h @@ -87,7 +87,7 @@ struct __type(key, __u32); __type(value, struct pdr_info); __uint(max_entries, PDR_MAP_SIZE); -} pdr_map_uplink_ip4 SEC(".maps"); +} pdr_map_teid_ip4 SEC(".maps"); enum far_action_mask { FAR_DROP = 0x01, From 5bc4b8daf5d42f770e628a124ef6718506b56a91 Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Sun, 13 Sep 2026 20:41:56 +0000 Subject: [PATCH 04/18] add simple downlink rebalancing to secondary UPF Signed-off-by: pirog-spb --- cmd/config/config.go | 6 +++++ cmd/ebpf/xdp/n3n6_entrypoint.c | 8 +++++++ cmd/ebpf/xdp/utils/routing.h | 43 +++++++++++++++++++++------------- cmd/main.go | 8 +++++++ 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 337b9352..bb0c07e7 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -113,6 +113,8 @@ type UpfConfig struct { TraceMaxDumpPackets int `mapstructure:"trace_max_packets" json:"trace_max_packets"` IP6RaSupport bool `mapstructure:"ip6_ra_support" json:"ip6_ra_support"` Ip6RaPrefix string `mapstructure:"ip6_ra_prefix" validate:"cidrv6" json:"ip6_ra_prefix"` + Ip4UEPrefix string `mapstructure:"ip4_ue_prefix" validate:"cidrv4" json:"ip4_ue_prefix"` + DLRebalanceAddress string `mapstructure:"rebalance_address" validate:"omitempty,ipv4" json:"rebalance_address"` n3AdvertisedAddress5G net.IP } @@ -183,6 +185,8 @@ func defineFlags() { pflag.Int("tracefpackets", 100000, "Maximum number of packets in one trace dump file") pflag.Bool("ip6ra", false, "Enable or disable IPv6 Router Advertisement support") pflag.String("ip6rapref", "2a03:d000:29a0:509::/64", "Subscriber IPv6 prefix") + pflag.String("ip4pref", "", "Subscriber IPv4 prefix") + pflag.String("rbaddr", "", "Downlink rebalace address") pflag.Parse() } @@ -264,6 +268,8 @@ func initCommonConfig() { _ = commonConfigV.BindPFlag("trace_max_packets", pflag.Lookup("tracefpackets")) _ = commonConfigV.BindPFlag("ip6_ra_support", pflag.Lookup("ip6ra")) _ = commonConfigV.BindPFlag("ip6_ra_prefix", pflag.Lookup("ip6rapref")) + _ = commonConfigV.BindPFlag("ip4_ue_prefix", pflag.Lookup("ip4pref")) + _ = commonConfigV.BindPFlag("rebalance_address", pflag.Lookup("rbaddr")) commonConfigV.SetDefault("n9_address", commonConfigV.GetString("n3_address")) commonConfigV.SetDefault("n3_adv_address", commonConfigV.GetString("n3_address")) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 535b5558..7cf90136 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -67,6 +67,9 @@ struct dataplane_config { __u8 trace_in; __u8 trace_out; __u8 trace_blocked; + __u32 rebalance_ip; + __u32 ue_subnet_prefix; + __u32 ue_subnet_prefix_mask; __u8 ip6_ra_support; __u128 ip6_ra_prefix; __u16 ip6_ra_prefix_length; @@ -207,6 +210,11 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte struct pdr_info *session = bpf_map_lookup_elem(&pdr_map_downlink_ip4, &ip4->daddr); if (!session) { upf_printk("upf: [n6] no downlink session for ip:%pI4", &ip4->daddr); + + if(global_config.rebalance_ip + && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) != global_config.ue_subnet_prefix) { + return route_ipv4_to(ctx->xdp_ctx, ctx->eth, ctx->ip4, global_config.rebalance_ip); + } return DEFAULT_XDP_ACTION; } diff --git a/cmd/ebpf/xdp/utils/routing.h b/cmd/ebpf/xdp/utils/routing.h index 73806d59..1b014400 100644 --- a/cmd/ebpf/xdp/utils/routing.h +++ b/cmd/ebpf/xdp/utils/routing.h @@ -87,8 +87,15 @@ static __always_inline void update_route_cache_ipv4(const struct bpf_fib_lookup } #endif -static __always_inline enum xdp_action do_route(struct xdp_md *ctx, struct ethhdr *eth, int ifindex, __u8 (*smac)[6], __u8 (*dmac)[6]) { - //_decr_ttl(ether_proto, l3hdr); +static __always_inline int decrease_ip_ttl(struct iphdr *iph) +{ + __u32 check = (__u32)iph->check; + check += (__u32)bpf_htons(0x0100); + iph->check = (__sum16)(check + (check >= 0xFFFF)); + return --iph->ttl; +} + +static __always_inline enum xdp_action forward(struct xdp_md *ctx, struct ethhdr *eth, int ifindex, __u8 (*smac)[6], __u8 (*dmac)[6]) { __builtin_memcpy(eth->h_source, smac, ETH_ALEN); __builtin_memcpy(eth->h_dest, dmac, ETH_ALEN); @@ -97,7 +104,7 @@ static __always_inline enum xdp_action do_route(struct xdp_md *ctx, struct ethhd return bpf_redirect(ifindex, 0); } -static __always_inline enum xdp_action route_ipv4(struct xdp_md *ctx, struct ethhdr *eth, const struct iphdr *ip4) { +static __always_inline enum xdp_action route_ipv4_to(struct xdp_md *ctx, struct ethhdr *eth, struct iphdr *ip4, __u32 daddr) { const __u32 key = 0; struct route_stat *statistic = bpf_map_lookup_elem(&upf_route_stat, &key); if (!statistic) { @@ -105,11 +112,11 @@ static __always_inline enum xdp_action route_ipv4(struct xdp_md *ctx, struct eth } #ifdef ENABLE_ROUTE_CACHE - struct route_record *cache = bpf_map_lookup_elem(&upf_route_cache_ip4, &ip4->daddr); + struct route_record *cache = bpf_map_lookup_elem(&upf_route_cache_ip4, &daddr); if (cache) { - upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: cached ifindex: %d", &ip4->saddr, &ip4->daddr, cache->ifindex); + upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: cached ifindex: %d", &ip4->saddr, &daddr, cache->ifindex); statistic->fib_lookup_ip4_cache += 1; - return do_route_ipv4(ctx, eth, cache->ifindex, &cache->smac, &cache->dmac); + return forward(ctx, eth, cache->ifindex, &cache->smac, &cache->dmac); } #endif @@ -121,26 +128,25 @@ static __always_inline enum xdp_action route_ipv4(struct xdp_md *ctx, struct eth fib_params.dport = 0; fib_params.tot_len = bpf_ntohs(ip4->tot_len); fib_params.ipv4_src = ip4->saddr; - fib_params.ipv4_dst = ip4->daddr; + fib_params.ipv4_dst = daddr; fib_params.ifindex = ctx->ingress_ifindex; int rc = bpf_fib_lookup(ctx, &fib_params, sizeof(fib_params), 0 /*BPF_FIB_LOOKUP_OUTPUT*/); switch (rc) { case BPF_FIB_LKUP_RET_SUCCESS: - upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: nexthop: %pI4", &ip4->saddr, &ip4->daddr, &fib_params.ipv4_dst); - upf_printk("upf: bpf_fib_lookup2 %pI4 -> %pI4: h_vlan_TCI: %d", &ip4->saddr, &ip4->daddr, fib_params.h_vlan_TCI); - upf_printk("upf: bpf_fib_lookup3 %pI4 -> %pI4: h_vlan_proto: %d", &ip4->saddr, &ip4->daddr, fib_params.h_vlan_proto); + upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: nexthop: %pI4", &ip4->saddr, &daddr, &fib_params.ipv4_dst); statistic->fib_lookup_ip4_ok += 1; #ifdef ENABLE_ROUTE_CACHE - update_route_cache_ipv4(&fib_params, ip4->daddr); + update_route_cache_ipv4(&fib_params, daddr); #endif - return do_route(ctx, eth, fib_params.ifindex, &fib_params.smac, &fib_params.dmac); + decrease_ip_ttl(ip4); + return forward(ctx, eth, fib_params.ifindex, &fib_params.smac, &fib_params.dmac); case BPF_FIB_LKUP_RET_BLACKHOLE: case BPF_FIB_LKUP_RET_UNREACHABLE: case BPF_FIB_LKUP_RET_PROHIBIT: - upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: %d", &ip4->saddr, &ip4->daddr, rc); + upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: %d", &ip4->saddr, &daddr, rc); statistic->fib_lookup_ip4_error_drop += 1; return XDP_DROP; case BPF_FIB_LKUP_RET_NOT_FWDED: @@ -149,13 +155,13 @@ static __always_inline enum xdp_action route_ipv4(struct xdp_md *ctx, struct eth case BPF_FIB_LKUP_RET_NO_NEIGH: case BPF_FIB_LKUP_RET_FRAG_NEEDED: default: - upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: %d", &ip4->saddr, &ip4->daddr, rc); + upf_printk("upf: bpf_fib_lookup %pI4 -> %pI4: %d", &ip4->saddr, &daddr, rc); statistic->fib_lookup_ip4_error_pass += 1; return XDP_PASS; /* Let's kernel takes care */ } } -static __always_inline enum xdp_action route_ipv6(struct xdp_md *ctx, struct ethhdr *eth, const struct ipv6hdr *ip6) { +static __always_inline enum xdp_action route_ipv6(struct xdp_md *ctx, struct ethhdr *eth, struct ipv6hdr *ip6) { const __u32 key = 0; struct route_stat *statistic = bpf_map_lookup_elem(&upf_route_stat, &key); if (!statistic) { @@ -179,7 +185,8 @@ static __always_inline enum xdp_action route_ipv6(struct xdp_md *ctx, struct eth upf_printk("upf: bpf_fib_lookup %pI6c -> %pI6c: nexthop: %pI6c", &ip6->saddr, &ip6->daddr, &fib_params.ipv6_dst); statistic->fib_lookup_ip6_ok += 1; - return do_route(ctx, eth, fib_params.ifindex, &fib_params.smac, &fib_params.dmac); + ip6->hop_limit--; + return forward(ctx, eth, fib_params.ifindex, &fib_params.smac, &fib_params.dmac); case BPF_FIB_LKUP_RET_BLACKHOLE: case BPF_FIB_LKUP_RET_UNREACHABLE: case BPF_FIB_LKUP_RET_PROHIBIT: @@ -196,4 +203,8 @@ static __always_inline enum xdp_action route_ipv6(struct xdp_md *ctx, struct eth statistic->fib_lookup_ip6_error_pass += 1; return XDP_PASS; /* Let's kernel takes care */ } +} + +static __always_inline enum xdp_action route_ipv4(struct xdp_md *ctx, struct ethhdr *eth, struct iphdr *ip4) { + return route_ipv4_to(ctx, eth, ip4, ip4->daddr); } \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 32af776f..bc8810ff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -143,6 +143,14 @@ func main() { entrypointConfig.Ip6RaPrefixLength = uint16(ip6PrefixLength) } + if config.Conf.DLRebalanceAddress != "" { + entrypointConfig.RebalanceIp = binary.LittleEndian.Uint32(net.ParseIP(config.Conf.DLRebalanceAddress).To4()) + + ip4Prefix, ip4Net, _ := net.ParseCIDR(config.Conf.Ip4UEPrefix) + entrypointConfig.UeSubnetPrefix = binary.LittleEndian.Uint32(ip4Prefix.To4()) + entrypointConfig.UeSubnetPrefixMask = binary.LittleEndian.Uint32(ip4Net.Mask) + } + if err := bpfObjects.GlobalConfig.Set(entrypointConfig); err != nil { log.Fatal().Err(err).Msgf("can't set dataplane global config") } From 9bf6377a565b31fa5857466951a22a874ab726a3 Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Tue, 15 Sep 2026 16:18:33 +0000 Subject: [PATCH 05/18] fix ue prefix matching Signed-off-by: pirog-spb --- cmd/ebpf/xdp/n3n6_entrypoint.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 7cf90136..f4448f10 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -212,7 +212,7 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte upf_printk("upf: [n6] no downlink session for ip:%pI4", &ip4->daddr); if(global_config.rebalance_ip - && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) != global_config.ue_subnet_prefix) { + && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) == global_config.ue_subnet_prefix) { return route_ipv4_to(ctx->xdp_ctx, ctx->eth, ctx->ip4, global_config.rebalance_ip); } return DEFAULT_XDP_ACTION; From 9e8f93375742275313a5a73b71bc4816dc6f99e7 Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Tue, 15 Sep 2026 17:31:28 +0000 Subject: [PATCH 06/18] add ipip encap tp send to secondary upf Signed-off-by: pirog-spb --- cmd/config/config.go | 3 ++ cmd/ebpf/xdp/n3n6_entrypoint.c | 61 +++++++++++++++++++++++++++++++++- cmd/main.go | 4 +++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index bb0c07e7..cd2a1db3 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -115,6 +115,7 @@ type UpfConfig struct { Ip6RaPrefix string `mapstructure:"ip6_ra_prefix" validate:"cidrv6" json:"ip6_ra_prefix"` Ip4UEPrefix string `mapstructure:"ip4_ue_prefix" validate:"cidrv4" json:"ip4_ue_prefix"` DLRebalanceAddress string `mapstructure:"rebalance_address" validate:"omitempty,ipv4" json:"rebalance_address"` + DLRebalanceLocalAddress string `mapstructure:"rebalance_local_address" validate:"omitempty,ipv4" json:"rebalance_local_address"` n3AdvertisedAddress5G net.IP } @@ -187,6 +188,7 @@ func defineFlags() { pflag.String("ip6rapref", "2a03:d000:29a0:509::/64", "Subscriber IPv6 prefix") pflag.String("ip4pref", "", "Subscriber IPv4 prefix") pflag.String("rbaddr", "", "Downlink rebalace address") + pflag.String("rbladdr", "", "Downlink rebalace local address") pflag.Parse() } @@ -270,6 +272,7 @@ func initCommonConfig() { _ = commonConfigV.BindPFlag("ip6_ra_prefix", pflag.Lookup("ip6rapref")) _ = commonConfigV.BindPFlag("ip4_ue_prefix", pflag.Lookup("ip4pref")) _ = commonConfigV.BindPFlag("rebalance_address", pflag.Lookup("rbaddr")) + _ = commonConfigV.BindPFlag("rebalance_local_address", pflag.Lookup("rbladdr")) commonConfigV.SetDefault("n9_address", commonConfigV.GetString("n3_address")) commonConfigV.SetDefault("n3_adv_address", commonConfigV.GetString("n3_address")) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index f4448f10..eebcf3b7 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -67,6 +67,7 @@ struct dataplane_config { __u8 trace_in; __u8 trace_out; __u8 trace_blocked; + __u32 rebalance_local_ip; __u32 rebalance_ip; __u32 ue_subnet_prefix; __u32 ue_subnet_prefix_mask; @@ -205,6 +206,61 @@ static __always_inline const struct pdr* check_sdf_filters_gtp(struct packet_con return 0; } +static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, int saddr, int daddr) { + + static const size_t ip_encap_size = sizeof(struct iphdr); + int ip_packet_len = 0; + __u16 ip_proto = 0; + if (ctx->ip4) { + ip_packet_len = bpf_ntohs(ctx->ip4->tot_len); + ip_proto = 4; + } + else if (ctx->ip6) { + ip_packet_len = bpf_ntohs(ctx->ip6->payload_len) + sizeof(struct ipv6hdr); + ip_proto = 41; + } + else + return -1; + + int result = bpf_xdp_adjust_head(ctx->xdp_ctx, (__s32)-ip_encap_size); + if (result) + return -1; + + char *data = (char *)(long)ctx->xdp_ctx->data; + const char *data_end = (const char *)(long)ctx->xdp_ctx->data_end; + + struct ethhdr *orig_eth = (struct ethhdr *)(data + ip_encap_size); + if ((const char *)(orig_eth + 1) > data_end) + return -1; + + struct ethhdr *eth = (struct ethhdr *)data; + __builtin_memcpy(eth, orig_eth, sizeof(*eth)); + eth->h_proto = bpf_htons(ETH_P_IP); + + struct iphdr *ip = (struct iphdr *)(eth + 1); + if ((const char *)(ip + 1) > data_end) + return -1; + + /* Add the outer IP header */ + ip->version = 4; + ip->ihl = 5; /* No options */ + ip->tos = 0; + ip->tot_len = bpf_htons(ip_packet_len + ip_encap_size); + ip->id = 0; /* No fragmentation */ + ip->frag_off = 0x0040; /* Don't fragment; Fragment offset = 0 */ + ip->ttl = 64; + ip->protocol = ip_proto; + ip->check = 0; + ip->saddr = saddr; + ip->daddr = daddr; + + ip->check = ipv4_csum(ip, sizeof(*ip)); + + /* Update packet pointers */ + context_set_ip4(ctx, (char *)(long)ctx->xdp_ctx->data, (const char *)(long)ctx->xdp_ctx->data_end, eth, ip, 0, 0); + return 0; +} + static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_context *ctx) { const struct iphdr *ip4 = ctx->ip4; struct pdr_info *session = bpf_map_lookup_elem(&pdr_map_downlink_ip4, &ip4->daddr); @@ -213,7 +269,10 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte if(global_config.rebalance_ip && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) == global_config.ue_subnet_prefix) { - return route_ipv4_to(ctx->xdp_ctx, ctx->eth, ctx->ip4, global_config.rebalance_ip); + if(global_config.rebalance_local_ip && 0 == encap_ip_packet(ctx, global_config.rebalance_local_ip, global_config.rebalance_ip)) + return route_ipv4(ctx->xdp_ctx, ctx->eth, ctx->ip4); + else + return route_ipv4_to(ctx->xdp_ctx, ctx->eth, ctx->ip4, global_config.rebalance_ip); } return DEFAULT_XDP_ACTION; } diff --git a/cmd/main.go b/cmd/main.go index bc8810ff..8999c83a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -146,6 +146,10 @@ func main() { if config.Conf.DLRebalanceAddress != "" { entrypointConfig.RebalanceIp = binary.LittleEndian.Uint32(net.ParseIP(config.Conf.DLRebalanceAddress).To4()) + if config.Conf.DLRebalanceLocalAddress != "" { + entrypointConfig.RebalanceLocalIp = binary.LittleEndian.Uint32(net.ParseIP(config.Conf.DLRebalanceLocalAddress).To4()) + } + ip4Prefix, ip4Net, _ := net.ParseCIDR(config.Conf.Ip4UEPrefix) entrypointConfig.UeSubnetPrefix = binary.LittleEndian.Uint32(ip4Prefix.To4()) entrypointConfig.UeSubnetPrefixMask = binary.LittleEndian.Uint32(ip4Net.Mask) From 675481dffeef73af3296b4d91668e427427d9fbd Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Tue, 15 Sep 2026 19:02:48 +0000 Subject: [PATCH 07/18] fix ebpf verifier Signed-off-by: pirog-spb --- cmd/ebpf/xdp/n3n6_entrypoint.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index eebcf3b7..4c11ff3c 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -206,11 +206,11 @@ static __always_inline const struct pdr* check_sdf_filters_gtp(struct packet_con return 0; } -static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, int saddr, int daddr) { +static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, __u32 saddr, __u32 daddr) { - static const size_t ip_encap_size = sizeof(struct iphdr); - int ip_packet_len = 0; - __u16 ip_proto = 0; + //static const size_t ip_encap_size = sizeof(struct iphdr); + size_t ip_packet_len = 0; + __u8 ip_proto = 0; if (ctx->ip4) { ip_packet_len = bpf_ntohs(ctx->ip4->tot_len); ip_proto = 4; @@ -222,14 +222,14 @@ static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, int sad else return -1; - int result = bpf_xdp_adjust_head(ctx->xdp_ctx, (__s32)-ip_encap_size); + int result = bpf_xdp_adjust_head(ctx->xdp_ctx, (__s32) - sizeof(struct iphdr)); if (result) return -1; char *data = (char *)(long)ctx->xdp_ctx->data; const char *data_end = (const char *)(long)ctx->xdp_ctx->data_end; - struct ethhdr *orig_eth = (struct ethhdr *)(data + ip_encap_size); + struct ethhdr *orig_eth = (struct ethhdr *)(data + sizeof(struct iphdr)); if ((const char *)(orig_eth + 1) > data_end) return -1; @@ -245,7 +245,7 @@ static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, int sad ip->version = 4; ip->ihl = 5; /* No options */ ip->tos = 0; - ip->tot_len = bpf_htons(ip_packet_len + ip_encap_size); + ip->tot_len = bpf_htons(ip_packet_len + sizeof(struct iphdr)); ip->id = 0; /* No fragmentation */ ip->frag_off = 0x0040; /* Don't fragment; Fragment offset = 0 */ ip->ttl = 64; From cf12a4ae3afd7889f2e9d9870137a3481aff346f Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Tue, 15 Sep 2026 20:16:29 +0000 Subject: [PATCH 08/18] fix ebpf verifier Signed-off-by: pirog-spb --- cmd/ebpf/xdp/n3n6_entrypoint.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 4c11ff3c..6336a003 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -229,11 +229,14 @@ static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, __u32 s char *data = (char *)(long)ctx->xdp_ctx->data; const char *data_end = (const char *)(long)ctx->xdp_ctx->data_end; + struct ethhdr *eth = (struct ethhdr *)data; + if ((const char *)(eth + 1) > data_end) + return -1; + struct ethhdr *orig_eth = (struct ethhdr *)(data + sizeof(struct iphdr)); if ((const char *)(orig_eth + 1) > data_end) return -1; - - struct ethhdr *eth = (struct ethhdr *)data; + __builtin_memcpy(eth, orig_eth, sizeof(*eth)); eth->h_proto = bpf_htons(ETH_P_IP); From 2a7ceded387dd29083c7d28fa91dfa5b154406d2 Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Tue, 15 Sep 2026 21:03:32 +0000 Subject: [PATCH 09/18] fix ebpf verifier Signed-off-by: pirog-spb --- cmd/ebpf/objects.go | 11 ++++++++++- cmd/ebpf/xdp/n3n6_entrypoint.c | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/ebpf/objects.go b/cmd/ebpf/objects.go index 9ac77586..f23da658 100644 --- a/cmd/ebpf/objects.go +++ b/cmd/ebpf/objects.go @@ -86,7 +86,7 @@ func (bpfObjects *BpfObjects) Load() error { "far_map": bpfObjects.farMapSize, "pdr_map_downlink_ip4": bpfObjects.pdrMapSize, "pdr_map_downlink_ip6": bpfObjects.pdrMapSize, - "pdr_map_teid_ip4": bpfObjects.pdrMapSize, + "pdr_map_teid_ip4": bpfObjects.pdrMapSize, "urr_map": bpfObjects.urrMapSize, } @@ -127,6 +127,15 @@ func (bpfObjects *BpfObjects) Load() error { m.Close() } + var ve *ebpf.VerifierError + if errors.As(err, &ve) { + for i, line := range ve.Log { + if line != "" { + log.Info().Msgf("[%03d] %s", i, line) + } + } + } + log.Warn().Msgf("Failed to load objects: %s", err) return err } diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 6336a003..44800d9f 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -272,8 +272,10 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte if(global_config.rebalance_ip && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) == global_config.ue_subnet_prefix) { - if(global_config.rebalance_local_ip && 0 == encap_ip_packet(ctx, global_config.rebalance_local_ip, global_config.rebalance_ip)) - return route_ipv4(ctx->xdp_ctx, ctx->eth, ctx->ip4); + if(global_config.rebalance_local_ip) { + if(0 == encap_ip_packet(ctx, global_config.rebalance_local_ip, global_config.rebalance_ip)) + return route_ipv4(ctx->xdp_ctx, ctx->eth, ctx->ip4); + } else return route_ipv4_to(ctx->xdp_ctx, ctx->eth, ctx->ip4, global_config.rebalance_ip); } From c1babd5f0ea9911540a99aa1895e5d688cab1f3c Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Fri, 25 Sep 2026 10:43:17 +0000 Subject: [PATCH 10/18] add ipip decap for redirected packets from the first upf Signed-off-by: pirog-spb --- cmd/ebpf/xdp/n3n6_entrypoint.c | 7 ++++++- cmd/ebpf/xdp/utils/packet_context.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 44800d9f..fdbf05f1 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -270,7 +270,8 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte if (!session) { upf_printk("upf: [n6] no downlink session for ip:%pI4", &ip4->daddr); - if(global_config.rebalance_ip + if(!ctx->redirected + && global_config.rebalance_ip && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) == global_config.ue_subnet_prefix) { if(global_config.rebalance_local_ip) { if(0 == encap_ip_packet(ctx, global_config.rebalance_local_ip, global_config.rebalance_ip)) @@ -604,6 +605,10 @@ static __always_inline enum xdp_action handle_gtpu(struct packet_context *ctx) { static __always_inline enum xdp_action handle_ip4(struct packet_context *ctx) { int l4_protocol = parse_ip4(ctx); + if(l4_protocol == IPPROTO_IPIP) { + l4_protocol = parse_ip4(ctx); + ctx->redirected = 1; + } switch (l4_protocol) { case IPPROTO_ICMP: { increment_counter(ctx->counters, rx_icmp); diff --git a/cmd/ebpf/xdp/utils/packet_context.h b/cmd/ebpf/xdp/utils/packet_context.h index 3381f8b8..3691b8f9 100644 --- a/cmd/ebpf/xdp/utils/packet_context.h +++ b/cmd/ebpf/xdp/utils/packet_context.h @@ -38,4 +38,5 @@ struct packet_context { struct tcphdr *tcp; struct gtpuhdr *gtp; __u8 trace; + __u8 redirected; }; From ca3479a0d9885968192c30a6db86806473e3744c Mon Sep 17 00:00:00 2001 From: Alex P Date: Sun, 27 Sep 2026 15:18:27 +0300 Subject: [PATCH 11/18] fix(config): guard rebalance setup against nil parse and invalid cidr --- cmd/main.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 8999c83a..c7405eeb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -144,13 +144,24 @@ func main() { } if config.Conf.DLRebalanceAddress != "" { - entrypointConfig.RebalanceIp = binary.LittleEndian.Uint32(net.ParseIP(config.Conf.DLRebalanceAddress).To4()) + rebalanceIP := net.ParseIP(config.Conf.DLRebalanceAddress).To4() + if rebalanceIP == nil { + log.Fatal().Msgf("invalid rebalance_address: %s", config.Conf.DLRebalanceAddress) + } + entrypointConfig.RebalanceIp = binary.LittleEndian.Uint32(rebalanceIP) if config.Conf.DLRebalanceLocalAddress != "" { - entrypointConfig.RebalanceLocalIp = binary.LittleEndian.Uint32(net.ParseIP(config.Conf.DLRebalanceLocalAddress).To4()) + rebalanceLocalIP := net.ParseIP(config.Conf.DLRebalanceLocalAddress).To4() + if rebalanceLocalIP == nil { + log.Fatal().Msgf("invalid rebalance_local_address: %s", config.Conf.DLRebalanceLocalAddress) + } + entrypointConfig.RebalanceLocalIp = binary.LittleEndian.Uint32(rebalanceLocalIP) } - ip4Prefix, ip4Net, _ := net.ParseCIDR(config.Conf.Ip4UEPrefix) + ip4Prefix, ip4Net, err := net.ParseCIDR(config.Conf.Ip4UEPrefix) + if err != nil { + log.Fatal().Err(err).Msgf("invalid ip4_ue_prefix: %s", config.Conf.Ip4UEPrefix) + } entrypointConfig.UeSubnetPrefix = binary.LittleEndian.Uint32(ip4Prefix.To4()) entrypointConfig.UeSubnetPrefixMask = binary.LittleEndian.Uint32(ip4Net.Mask) } From 08bb461761569302d17df6da0b8f0df242ee6fdc Mon Sep 17 00:00:00 2001 From: Alex P Date: Sun, 27 Sep 2026 15:18:27 +0300 Subject: [PATCH 12/18] fix(xdp): drop redirected packets on session miss instead of pass --- cmd/ebpf/xdp/n3n6_entrypoint.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index fdbf05f1..ef6ca62a 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -270,8 +270,12 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte if (!session) { upf_printk("upf: [n6] no downlink session for ip:%pI4", &ip4->daddr); - if(!ctx->redirected - && global_config.rebalance_ip + if(ctx->redirected) { + upf_printk("upf: [n6] drop redirected packet: no session for ip:%pI4", &ip4->daddr); + return XDP_DROP; + } + + if(global_config.rebalance_ip && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) == global_config.ue_subnet_prefix) { if(global_config.rebalance_local_ip) { if(0 == encap_ip_packet(ctx, global_config.rebalance_local_ip, global_config.rebalance_ip)) From e8fb7d38539019831d424b5e84ddaab5fe232f20 Mon Sep 17 00:00:00 2001 From: Alex P Date: Sun, 27 Sep 2026 15:35:35 +0300 Subject: [PATCH 13/18] fix(xdp): overwrite ipip outer header when adding gtp encap in single adjust --- cmd/ebpf/xdp/utils/gtp_utils.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/ebpf/xdp/utils/gtp_utils.h b/cmd/ebpf/xdp/utils/gtp_utils.h index a3c0c43d..46a881d8 100644 --- a/cmd/ebpf/xdp/utils/gtp_utils.h +++ b/cmd/ebpf/xdp/utils/gtp_utils.h @@ -207,6 +207,9 @@ static __always_inline __u32 add_gtp_over_ip4_headers(struct packet_context *ctx gtp_full_hdr_size = gtp_full_hdr_ext_size; } + __u8 ipip_overhead = ctx->redirected ? sizeof(struct iphdr) : 0; + size_t adjust_size = gtp_encap_size - ipip_overhead; + // #ifdef NO_GTP_EXTENTION // static const size_t gtp_full_hdr_size = sizeof(struct gtpuhdr); // static const size_t gtp_encap_size = sizeof(struct iphdr) + sizeof(struct udphdr) + gtp_full_hdr_size; @@ -225,14 +228,14 @@ static __always_inline __u32 add_gtp_over_ip4_headers(struct packet_context *ctx else return -1; - int result = bpf_xdp_adjust_head(ctx->xdp_ctx, (__s32)-gtp_encap_size); + int result = bpf_xdp_adjust_head(ctx->xdp_ctx, (__s32)-adjust_size); if (result) return -1; char *data = (char *)(long)ctx->xdp_ctx->data; const char *data_end = (const char *)(long)ctx->xdp_ctx->data_end; - struct ethhdr *orig_eth = (struct ethhdr *)(data + gtp_encap_size); + struct ethhdr *orig_eth = (struct ethhdr *)(data + adjust_size); if ((const char *)(orig_eth + 1) > data_end) return -1; From eecf2c14c2ea585a10c58ba804e3953992ef1dc3 Mon Sep 17 00:00:00 2001 From: Alex P Date: Sun, 27 Sep 2026 23:52:01 +0300 Subject: [PATCH 14/18] fix(xdp): disable unsupported ipv6-in-ipv4 branch in ipip encap --- cmd/ebpf/xdp/n3n6_entrypoint.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index ef6ca62a..3b221cf2 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -215,10 +215,10 @@ static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, __u32 s ip_packet_len = bpf_ntohs(ctx->ip4->tot_len); ip_proto = 4; } - else if (ctx->ip6) { - ip_packet_len = bpf_ntohs(ctx->ip6->payload_len) + sizeof(struct ipv6hdr); - ip_proto = 41; - } + // else if (ctx->ip6) { + // ip_packet_len = bpf_ntohs(ctx->ip6->payload_len) + sizeof(struct ipv6hdr); + // ip_proto = 41; + // } else return -1; From 70cc2fc0c0ff7c1fb3b8657cd8dc628cf5affd40 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 28 Sep 2026 18:45:12 +0300 Subject: [PATCH 15/18] fix(xdp): rebalance downlink on stale idle session with non-forward far --- cmd/ebpf/xdp/n3n6_entrypoint.c | 35 ++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 3b221cf2..77aec097 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -264,6 +264,19 @@ static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, __u32 s return 0; } +static __always_inline int try_rebalance(struct packet_context *ctx) { + if(!global_config.rebalance_ip + || (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) != global_config.ue_subnet_prefix) + return -1; + + if(global_config.rebalance_local_ip) { + if(0 == encap_ip_packet(ctx, global_config.rebalance_local_ip, global_config.rebalance_ip)) + return route_ipv4(ctx->xdp_ctx, ctx->eth, ctx->ip4); + return -1; + } + return route_ipv4_to(ctx->xdp_ctx, ctx->eth, ctx->ip4, global_config.rebalance_ip); +} + static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_context *ctx) { const struct iphdr *ip4 = ctx->ip4; struct pdr_info *session = bpf_map_lookup_elem(&pdr_map_downlink_ip4, &ip4->daddr); @@ -275,15 +288,9 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte return XDP_DROP; } - if(global_config.rebalance_ip - && (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) == global_config.ue_subnet_prefix) { - if(global_config.rebalance_local_ip) { - if(0 == encap_ip_packet(ctx, global_config.rebalance_local_ip, global_config.rebalance_ip)) - return route_ipv4(ctx->xdp_ctx, ctx->eth, ctx->ip4); - } - else - return route_ipv4_to(ctx->xdp_ctx, ctx->eth, ctx->ip4, global_config.rebalance_ip); - } + int rebalance_action = try_rebalance(ctx); + if(rebalance_action != -1) + return rebalance_action; return DEFAULT_XDP_ACTION; } @@ -316,8 +323,16 @@ static __always_inline enum xdp_action handle_n6_packet_ipv4(struct packet_conte } // Only forwarding action is supported at the moment - if (!(far->action & FAR_FORW)) + if (!(far->action & FAR_FORW)) { + if(ctx->redirected) { + upf_printk("upf: [n6] drop redirected: non-forward session ip:%pI4", &ip4->daddr); + return XDP_DROP; + } + int rebalance_action = try_rebalance(ctx); + if(rebalance_action != -1) + return rebalance_action; return XDP_DROP; + } // Only outer header GTP/UDP/IPv4 is supported at the moment if (!(far->outer_header_creation & OHC_GTP_U_UDP_IPv4)) From d1a5ed7872033a6d81e0fa206e426a96f0146e6e Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Wed, 21 Oct 2026 12:05:11 +0300 Subject: [PATCH 16/18] feat(ebpf): support multiple UE subnets for downlink rebalance Replace the single ue_subnet_prefix/mask pair in dataplane config with an LPM trie (ue_rebalance_lpm_map) so several APN subscriber prefixes can be matched for downlink rebalance. Config key ip4_ue_prefix is replaced by ip4_ue_prefixes (repeatable --ip4prefs flag). --- cmd/config/config.go | 6 +- cmd/ebpf/objects.go | 1 + cmd/ebpf/objects_test.go | 107 +++++++ cmd/ebpf/rebalance.go | 27 ++ cmd/ebpf/xdp/n3n6_entrypoint.c | 13 +- cmd/ebpf/xdp/rebalance.h | 35 +++ cmd/main.go | 12 +- docs/docs-ru_ru/redundancy_scheme.md | 453 +++++++++++++++++++++++++++ 8 files changed, 642 insertions(+), 12 deletions(-) create mode 100644 cmd/ebpf/rebalance.go create mode 100644 cmd/ebpf/xdp/rebalance.h create mode 100644 docs/docs-ru_ru/redundancy_scheme.md diff --git a/cmd/config/config.go b/cmd/config/config.go index cd2a1db3..183af0f7 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -113,7 +113,7 @@ type UpfConfig struct { TraceMaxDumpPackets int `mapstructure:"trace_max_packets" json:"trace_max_packets"` IP6RaSupport bool `mapstructure:"ip6_ra_support" json:"ip6_ra_support"` Ip6RaPrefix string `mapstructure:"ip6_ra_prefix" validate:"cidrv6" json:"ip6_ra_prefix"` - Ip4UEPrefix string `mapstructure:"ip4_ue_prefix" validate:"cidrv4" json:"ip4_ue_prefix"` + Ip4UEPrefixes []string `mapstructure:"ip4_ue_prefixes" validate:"omitempty,dive,cidrv4" json:"ip4_ue_prefixes"` DLRebalanceAddress string `mapstructure:"rebalance_address" validate:"omitempty,ipv4" json:"rebalance_address"` DLRebalanceLocalAddress string `mapstructure:"rebalance_local_address" validate:"omitempty,ipv4" json:"rebalance_local_address"` @@ -186,7 +186,7 @@ func defineFlags() { pflag.Int("tracefpackets", 100000, "Maximum number of packets in one trace dump file") pflag.Bool("ip6ra", false, "Enable or disable IPv6 Router Advertisement support") pflag.String("ip6rapref", "2a03:d000:29a0:509::/64", "Subscriber IPv6 prefix") - pflag.String("ip4pref", "", "Subscriber IPv4 prefix") + pflag.StringArray("ip4prefs", []string{}, "Subscriber IPv4 prefixes") pflag.String("rbaddr", "", "Downlink rebalace address") pflag.String("rbladdr", "", "Downlink rebalace local address") @@ -270,7 +270,7 @@ func initCommonConfig() { _ = commonConfigV.BindPFlag("trace_max_packets", pflag.Lookup("tracefpackets")) _ = commonConfigV.BindPFlag("ip6_ra_support", pflag.Lookup("ip6ra")) _ = commonConfigV.BindPFlag("ip6_ra_prefix", pflag.Lookup("ip6rapref")) - _ = commonConfigV.BindPFlag("ip4_ue_prefix", pflag.Lookup("ip4pref")) + _ = commonConfigV.BindPFlag("ip4_ue_prefixes", pflag.Lookup("ip4prefs")) _ = commonConfigV.BindPFlag("rebalance_address", pflag.Lookup("rbaddr")) _ = commonConfigV.BindPFlag("rebalance_local_address", pflag.Lookup("rbladdr")) diff --git a/cmd/ebpf/objects.go b/cmd/ebpf/objects.go index f23da658..e3905ae9 100644 --- a/cmd/ebpf/objects.go +++ b/cmd/ebpf/objects.go @@ -88,6 +88,7 @@ func (bpfObjects *BpfObjects) Load() error { "pdr_map_downlink_ip6": bpfObjects.pdrMapSize, "pdr_map_teid_ip4": bpfObjects.pdrMapSize, "urr_map": bpfObjects.urrMapSize, + "ue_rebalance_lpm_map": 16, } replacements := make(map[string]*ebpf.Map) diff --git a/cmd/ebpf/objects_test.go b/cmd/ebpf/objects_test.go index 1bed08c8..756dbc1e 100644 --- a/cmd/ebpf/objects_test.go +++ b/cmd/ebpf/objects_test.go @@ -1134,6 +1134,113 @@ func TestEntrypoint(t *testing.T) { } +var ( + rebalanceIP = net.ParseIP("10.99.0.2") + rebalanceLocalIP = net.ParseIP("10.99.0.1") +) + +func testDownlinkRebalancePacket(bpfObjects *BpfObjects, dstIP net.IP) (int, []byte, error) { + packet := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(packet, gopacket.SerializeOptions{}, + &layers.Ethernet{ + SrcMAC: n9MAC, + DstMAC: n3MAC, + EthernetType: layers.EthernetTypeIPv4, + }, + &layers.IPv4{ + Version: 4, + DstIP: dstIP, + SrcIP: net.IP{1, 1, 1, 1}, + Protocol: layers.IPProtocolICMPv4, + IHL: 5, + TTL: 64, + }, + &layers.ICMPv4{ + TypeCode: layers.ICMPv4TypeEchoRequest, + }, + ); err != nil { + return 0, nil, fmt.Errorf("serializing input packet failed: %v", err) + } + + return bpfObjects.UpfIpEntrypointFunc.Test(append(packet.Bytes(), 0x00, 0x00, 0x00, 0x00)) +} + +func TestDownlinkRebalance(t *testing.T) { + + if err := IncreaseResourceLimits(); err != nil { + t.Fatalf("Can't increase resource limits: %s", err.Error()) + } + + bpfObjects := NewBpfObjects() + + if err := bpfObjects.Load(); err != nil { + t.Fatalf("Loading bpf objects failed: %s", err.Error()) + } + + defer bpfObjects.Close() + + entrypointConfig := IpEntrypointDataplaneConfig{ + N3Ipv4Address: binary.LittleEndian.Uint32(n3IP.To4()), + N9Ipv4Address: binary.LittleEndian.Uint32(n9IP.To4()), + RebalanceIp: binary.LittleEndian.Uint32(rebalanceIP.To4()), + RebalanceLocalIp: binary.LittleEndian.Uint32(rebalanceLocalIP.To4()), + } + + if err := bpfObjects.GlobalConfig.Set(entrypointConfig); err != nil { + t.Fatalf("can't set dataplane global config: %s", err.Error()) + } + + prefixes := []string{"10.60.0.0/24", "10.61.0.0/16"} + for _, prefix := range prefixes { + if err := PutUeRebalancePrefix(bpfObjects.UeRebalanceLpmMap, prefix); err != nil { + t.Fatalf("can't put rebalance prefix %s: %s", prefix, err.Error()) + } + } + + inPrefix := []net.IP{ + net.IP{10, 60, 0, 1}, + net.IP{10, 60, 0, 254}, + net.IP{10, 61, 128, 7}, + } + for _, dstIP := range inPrefix { + bpfRet, bufOut, err := testDownlinkRebalancePacket(bpfObjects, dstIP) + if err != nil { + t.Fatalf("ebpf run failed: %v", err) + } + if bpfRet != 4 { // XDP_REDIRECT + t.Fatalf("unexpected return value for %s: %d", dstIP, bpfRet) + } + + response := gopacket.NewPacket(bufOut, layers.LayerTypeEthernet, gopacket.Default) + ipLayer := response.Layer(layers.LayerTypeIPv4) + if ipLayer == nil { + t.Fatalf("no ipv4 layer in output packet for %s", dstIP) + } + outerIP, _ := ipLayer.(*layers.IPv4) + if !outerIP.DstIP.Equal(rebalanceIP) || !outerIP.SrcIP.Equal(rebalanceLocalIP) { + t.Fatalf("unexpected outer ipip header: %s -> %s", outerIP.SrcIP, outerIP.DstIP) + } + if outerIP.Protocol != layers.IPProtocolIPv4 { + t.Fatalf("unexpected outer protocol: %d", outerIP.Protocol) + } + } + + outOfPrefix := []net.IP{ + net.IP{10, 60, 1, 1}, + net.IP{10, 62, 0, 1}, + net.IP{8, 8, 8, 8}, + } + for _, dstIP := range outOfPrefix { + bpfRet, _, err := testDownlinkRebalancePacket(bpfObjects, dstIP) + if err != nil { + t.Fatalf("ebpf run failed: %v", err) + } + if bpfRet != 2 { // XDP_PASS + t.Fatalf("unexpected return value for %s: %d", dstIP, bpfRet) + } + } +} + func TestEntrypointBenchmark(t *testing.T) { if err := IncreaseResourceLimits(); err != nil { diff --git a/cmd/ebpf/rebalance.go b/cmd/ebpf/rebalance.go new file mode 100644 index 00000000..cdac708d --- /dev/null +++ b/cmd/ebpf/rebalance.go @@ -0,0 +1,27 @@ +package ebpf + +import ( + "net" + "unsafe" + + "github.com/cilium/ebpf" +) + +type UeRebalanceLpmKey struct { + PrefixLen uint32 + Data [4]byte +} + +func PutUeRebalancePrefix(m *ebpf.Map, cidr string) error { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + ones, _ := ipNet.Mask.Size() + key := UeRebalanceLpmKey{ + PrefixLen: uint32(ones), + } + copy(key.Data[:], ipNet.IP.To4()) + var value uint8 = 1 + return m.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), ebpf.UpdateAny) +} diff --git a/cmd/ebpf/xdp/n3n6_entrypoint.c b/cmd/ebpf/xdp/n3n6_entrypoint.c index 77aec097..d8cdf2eb 100644 --- a/cmd/ebpf/xdp/n3n6_entrypoint.c +++ b/cmd/ebpf/xdp/n3n6_entrypoint.c @@ -31,6 +31,7 @@ #include "xdp/qer.h" #include "xdp/urr.h" #include "xdp/pdr.h" +#include "xdp/rebalance.h" #include "xdp/sdf_filter.h" #include "xdp/utils/common.h" @@ -69,8 +70,6 @@ struct dataplane_config { __u8 trace_blocked; __u32 rebalance_local_ip; __u32 rebalance_ip; - __u32 ue_subnet_prefix; - __u32 ue_subnet_prefix_mask; __u8 ip6_ra_support; __u128 ip6_ra_prefix; __u16 ip6_ra_prefix_length; @@ -265,8 +264,14 @@ static __always_inline __u32 encap_ip_packet(struct packet_context *ctx, __u32 s } static __always_inline int try_rebalance(struct packet_context *ctx) { - if(!global_config.rebalance_ip - || (ctx->ip4->daddr & global_config.ue_subnet_prefix_mask) != global_config.ue_subnet_prefix) + if(!global_config.rebalance_ip || !ctx->ip4) + return -1; + + struct ue_rebalance_lpm_key key = { + .prefix_len = 32, + }; + __builtin_memcpy(key.data, &ctx->ip4->daddr, sizeof(key.data)); + if(!bpf_map_lookup_elem(&ue_rebalance_lpm_map, &key)) return -1; if(global_config.rebalance_local_ip) { diff --git a/cmd/ebpf/xdp/rebalance.h b/cmd/ebpf/xdp/rebalance.h new file mode 100644 index 00000000..2a0f4565 --- /dev/null +++ b/cmd/ebpf/xdp/rebalance.h @@ -0,0 +1,35 @@ +/** + * Copyright 2026 Edgecom LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +/* LPM trie key: prefix length in bits followed by the IPv4 prefix in network byte order */ +struct ue_rebalance_lpm_key { + __u32 prefix_len; + __u8 data[4]; +}; + +/* UE prefixes eligible for downlink rebalance */ +struct +{ + __uint(type, BPF_MAP_TYPE_LPM_TRIE); + __type(key, struct ue_rebalance_lpm_key); + __type(value, __u8); + __uint(max_entries, 16); +} ue_rebalance_lpm_map SEC(".maps"); diff --git a/cmd/main.go b/cmd/main.go index c7405eeb..20ceb3b5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -158,12 +158,14 @@ func main() { entrypointConfig.RebalanceLocalIp = binary.LittleEndian.Uint32(rebalanceLocalIP) } - ip4Prefix, ip4Net, err := net.ParseCIDR(config.Conf.Ip4UEPrefix) - if err != nil { - log.Fatal().Err(err).Msgf("invalid ip4_ue_prefix: %s", config.Conf.Ip4UEPrefix) + if len(config.Conf.Ip4UEPrefixes) == 0 { + log.Fatal().Msgf("rebalance_address is set but no ip4_ue_prefixes configured") + } + for _, prefix := range config.Conf.Ip4UEPrefixes { + if err := ebpf.PutUeRebalancePrefix(bpfObjects.UeRebalanceLpmMap, prefix); err != nil { + log.Fatal().Err(err).Msgf("invalid ip4_ue_prefixes entry: %s", prefix) + } } - entrypointConfig.UeSubnetPrefix = binary.LittleEndian.Uint32(ip4Prefix.To4()) - entrypointConfig.UeSubnetPrefixMask = binary.LittleEndian.Uint32(ip4Net.Mask) } if err := bpfObjects.GlobalConfig.Set(entrypointConfig); err != nil { diff --git a/docs/docs-ru_ru/redundancy_scheme.md b/docs/docs-ru_ru/redundancy_scheme.md new file mode 100644 index 00000000..aa733063 --- /dev/null +++ b/docs/docs-ru_ru/redundancy_scheme.md @@ -0,0 +1,453 @@ +# Схема резервирования UPF в двух конвергентных pLTE/p5G ядрах + +Документ описывает два варианта резервирования пользовательской плоскости (UPF) +для связки из **двух конвергентных ядер** (EPC + 5GC в каждом), подключённых к +одной базовой станции gNB. Варианты различаются способом маршрутизации downlink +и сохранением IP-адреса абонента при отказе ядра. + +- **Вариант A — разделённые пулы (prefix-routing).** Непересекающиеся пулы + IP-адресов абонентов, маршрутизация в DN по префиксу. Не требует изменений в + работе системы. Два ядра сети работают полностью независимо. +- **Вариант B — общий пул IP-адресов (ECMP + IPIP miss-forward).** Общий пул + адресов, ECMP в DN на оба ядра, пересылка «чужого» downlink между ядрами через + IPIP. Статически назначаемые IP-адреса при отказе ядра сохраняются, т.к. + выведены в общий пул. + +Оба варианта работают в режиме **Active-Active** (оба ядра одновременно +обслуживают своих абонентов) и **не** обеспечивают бесшовного переключения +сессий: при отказе якорного ядра абонент выполняет повторную регистрацию +(re-attach) в резервном ядре. + +--- + +## 1. Назначение и область применения + +Цель схемы — повысить доступность сервиса передачи данных пользовательского +трафика, когда одна gNB или набор gNB одновременно обслуживаются двумя +независимыми конвергентными ядрами. + +Схема обеспечивает: + +- **Резервирование маршрута downlink** (направление к UE): при отказе одного ядра + абоненты восстанавливают обслуживание через резервное ядро. +- **Горячий резерв ядра**: второе ядро постоянно активно и готово принять + абонентов отказавшего. + +**Важно:** переключение **не бесшовное**. Состояние PDU-сессии (PDR/FAR/QER, +N3 TEID, NAS-контекст) существует только в ядре-якоре. При отказе якоря абонент +выполняет повторную регистрацию в живом ядре: повторная аутентификация +(5G AKA / EAP-AKA'), новая PDU-сессия, возможна смена IP. RTO — секунды. Это +осознанное ограничение схемы; репликация состояния сессий между ядрами не +предусмотрена (не стандартизована в 3GPP). + +--- + +## 2. Термины и сокращения + +| Термин | Определение | +|---|---| +| **Конвергентное ядро** | ядро, объединяющее функции EPC (LTE) и 5GC (5G); gNB подключается по NG к 5GC-стороне. | +| **DN** | Data Network — внешняя сеть данных (N6 / SGi). | +| **gNB** | базовая станция NR, подключённая по NG к 5GC-стороне каждого ядра. | +| **PDR / FAR / QER** | Packet Detection Rule / Forwarding Action Rule / QoS Enforcement Rule — элементы состояния сессии. | +| **N3** | интерфейс RAN↔UPF (GTP-U). | +| **N6** | интерфейс UPF↔DN (IP). | +| **N3 TEID** | идентификатор туннеля GTP-U, назначаемый SMF для PDU-сессии. | +| **IPIP** | инкапсуляция IPv4-в-IPv4 (IP-in-IP, протокол 4) или IPv6-в-IPv4. | +| **Miss-правило (catch-all)** | правило «по умолчанию»: при отсутствии сессии по DST IP пакет пересылается пиру (Вариант B). | +| **Якорное ядро** | ядро, в котором фактически создана PDU-сессия абонента (хранит PDR/TEID). | +| **Prefix-routing** | маршрутизация downlink по префиксу пула абонентских IP (Вариант A). | + +--- + +## 3. Архитектура и топология + +Общая топология для обоих вариантов. Различие — в поведении DN и наличии +IPIP-связи (только Вариант B). + +``` + ┌─────────────────────────────────────────────┐ + │ Data Network (DN) │ + │ A: prefix-routing по пулу B: ECMP per-flow │ + └────────────────┬──────────────┬──────────────┘ + │ │ + [маршрут к UPF-A] [маршрут к UPF-B] + │ │ + ┌────────────────────────────▼──┐ ┌────▼────────────────────────┐ + │ Ядро А (конвергентное) │ │ Ядро Б (конвергентное) │ + │ ┌──────────────────────────┐ │ │ ┌─────────────────────────┐│ + │ │ AMF/SMF (EPC+5GC) │ │ │ │ AMF/SMF (EPC+5GC) ││ + │ └────────────┬─────────────┘ │ │ └────────────┬────────────┘│ + │ │ PFCP (N4) │ │ │ PFCP (N4) │ + │ ┌────────────▼─────────────┐ │ │ ┌────────────▼────────────┐│ + │ │ UPF-A (XDP) │◄┼──────┼─►│ UPF-B (XDP) ││ + │ └────────────┬─────────────┘ │ │ └────────────┬────────────┘│ + └───────────────┼───────────────┘ └───────────────┼────────────┘ + │ N3 (GTP-U) N3 (GTP-U)│ + │ IPIP-связь │ + │ (только Вариант B, │ + │ 2+ линка, ECMP) │ + ┌────▼────────────────────────────────────────▼────┐ + │ gNB │ + │ NG → 5GC ядра А, NG → 5GC ядра Б │ + └──────────────────────────────────────────────────┘ +``` + +Ключевые элементы: + +1. **gNB подключена к обоим ядрам** по NG (к 5GC-стороне каждого конвергентного + ядра). gNB знает, в каком ядре создана каждая PDU-сессия (по назначенному + N3 TEID), поэтому **uplink всегда уходит в якорное ядро** — пересылка uplink + между ядрами не требуется в обоих вариантах. +2. **DN → UPF:** + - Вариант A: маршрутизация по префиксу (пул A → UPF-A, пул B → UPF-B). + - Вариант B: ECMP на оба UPF, хеширование строго **per-flow (5-tuple)**. +3. **IPIP-связь UPF-A ↔ UPF-B** — только Вариант B. Рекомендуются **2+ + независимых линка (ECMP/LACP)**; при одном линке — единая точка отказа (SPOF). + +--- + +## 4. Два варианта схемы + +### 4.1. Сводное сравнение + +| Свойство | Вариант A — разделённые пулы | Вариант B — общий пул | +|---|---|---| +| Маршрутизация DL в DN | по префиксу пула | ECMP per-flow | +| Дополнительный межядерный DL-трафик | **Отсутствует** (IPIP не нужен) | ~50% (постоянно) | +| Поддержка в UPF | **не требуется** | требуется (miss→IPIP, decap, anti-loop) | +| IPIP-связь | не используется | требуется (2+ линка) | +| Статический IP при отказе ядра | **не сохраняется** (UE получает IP из пула живого ядра) | **сохраняется** (IP из подписки, общий пул) | +| Динамический IP при отказе | новый IP из чужого пула | новый IP (re-attach), но из того же пула | +| Риск коллизий IP | исключён (пулы непересекающиеся) | есть для динамики — нужен механизм координации | +| RTO при отказе ядра | re-attach, секунды | re-attach, секунды | +| Подходит для | динамических абонентов | статических IP, требования сохранить IP | + +### 4.2. Вариант A — разделённые пулы (prefix-routing) + +Ядрам назначаются **непересекающиеся пулы** абонентских IP: пул A → ядро A, +пул B → ядро B. DN маршрутизирует downlink по префиксу: пакеты с DST IP из пула A +идут на UPF-A, из пула B — на UPF-B. Каждый UPF получает только «свой» +downlink и обрабатывает его штатно — без пересылки. + +При отказе ядра A абоненты пула A выполняют re-attach в ядро B и получают адреса +из пула B. DN начинает маршрутизировать их трафик на UPF-B. Сохранить старый IP +из пула A невозможно (он принадлежит отказавшему ядру). + +Достоинства: нулевой межядерный трафик, отсутствие IPIP, никаких дополнительных +требований в UPF, нет риска коллизий IP. + +### 4.3. Вариант B — общий пул (ECMP + IPIP miss-forward) + +Оба ядра используют **общий пул** абонентских IP. DN ведёт ECMP на оба UPF. +Из-за этого часть downlink-пакетов абонента из ядра A попадает на UPF-B. +UPF-B не находит сессию по DST IP и по miss-правилу пересылает пакет через IPIP +на UPF-A, который обрабатывает его штатно. + +При отказе ядра A абоненты выполняют re-attach в ядро B. Поскольку пул общий, +**статически назначенные IP сохраняются** (IP из подписки действителен в обоих +ядрах). DN продолжает ECMP, теперь UPF-B находит сессии у себя — пересылка +прекращается. + +**Риск коллизий для динамических IP:** два ядра могут одновременно выделить +одинаковый IP разным абонентам. Требуется координация: непересекающиеся +динамические субпулы внутри общего пула, либо централизованное выделение адресов. +Для статических IP (уникальны по подписке) риск отсутствует. + +Достоинства: сохранение статических IP при отказе, единая конфигурация пула. +Недостатки: постоянный межядерный трафик (~50% DL), требуется поддержка в UPF, +риск коллизий динамики. + +--- + +## 5. Потоки трафика + +### 5.1. Uplink (UE → DN) — общий для обоих вариантов + +gNB знает якорный UPF для каждой PDU-сессии (по N3 TEID, выданному SMF). Трафик +GTP-U с нужным TEID направляется **только** в UPF якорного ядра. UPF находит +сессию по TEID, декапсулирует GTP-U и маршрутизирует внутренний IP-пакет в DN. +Пересылка между UPF в uplink **не используется**. + +### 5.2. Downlink — Вариант A + +DN шлёт IP-пакет с DST IP = IP абонента. По префиксу пакеты пулa A идут на +UPF-A, пулa B — на UPF-B. UPF находит сессию по DST IP и инкапсулирует в GTP-U +(N3) для отправки в gNB. Штатный путь, без пересылки. + +### 5.3. Downlink — Вариант B + +**Локальный (сессия «своя»).** DN шлёт пакет на UPF, где сессия создана. +UPF находит её по DST IP, инкапсулирует в GTP-U (N3), отправляет в gNB. + +**Через пира (сессия «чужая»).** Из-за ECMP пакет абонента из ядра A попадает на +UPF-B. UPF-B не находит сессию по DST IP. По miss-правилу инкапсулирует +**исходный IP-пакет** в IPIP и отправляет на UPF-A. UPF-A деинкапсулирует IPIP, +получает «чистый» IP-пакет и обрабатывает как обычный N6-трафик: находит сессию +по DST IP, инкапсулирует в GTP-U (N3), отправляет в gNB. + +``` +DN ──[DST IP = UE]──► UPF-B (нет сессии) + │ IPIP-encap + ▼ + UPF-A (есть сессия) + │ N3 GTP-U + ▼ + gNB ──► UE +``` + +--- + +## 6. Особенности механизма пересылки (Вариант B) + +### 6.1. Идентификация сессии + +- **Downlink (N6):** поиск сессии по DST IP. +- **Uplink (N3):** поиск сессии по N3 TEID. Пересылка по uplink не выполняется. + +### 6.2. Miss-правило (catch-all) + +Текущее поведение при отсутствии сессии — пакет передаётся ядру ОС. В Варианте B +это поведение заменяется: + +1. Пакет пришёл **со стороны N6** (внешняя сеть DN), сессия **не найдена**, + DST IP **не является локальным** (см. 6.5) и пакет **unicast** (см. 6.7): + → **IPIP-инкапсуляция** и отправка на адрес пира UPF. +2. Пакет пришёл **со стороны IPIP-интерфейса** (уже переслан пиром), сессия + **всё ещё не найдена**: + → **`DROP`** (защита от циклов, см. 6.4). + +### 6.3. IPIP-инкапсуляция / деинкапсуляция + +- **Инкапсуляция (на UPF, не найдена сессия):** поверх исходного IP-пакета + добавляется внешний IPv4-заголовок: + - `src` = локальный адрес UPF на IPIP-линке; + - `dst` = адрес пира UPF на IPIP-линке; + - `protocol` = 4 (IPIP). +- **Деинкапсуляция (на пире):** входящий пакет с `protocol == 4` на + IPIP-интерфейсе разбирается, внешний IP-заголовок снимается, внутренний + IP-пакет передаётся в обычный downlink-обработчик + (`handle_n6_packet_ipv4` / `handle_n6_packet_ipv6`). + +IPIP выбран вместо GTP-U (N9) сознательно: после декапсуляции пакет обрабатывается +стандартным N6-путём по DST IP, **без необходимости создавать и синхронизировать +дополнительные PDR с ключом TEID** между ядрами. + +### 6.4. Защита от циклов (anti-loop) + +Риск: если IP не находит ни в одном UPF (расхождение конфигураций, ошибочно +маршрутизированный пакет), наивная логика «miss → переслать» приведёт к +бесконечному ping-pong A↔B. + +**Защита:** источник пакета определяет действие. Пакет, прибывший **уже через +IPIP-интерфейс** (т.е. пересланный), при повторном miss-совпадении **выбрасывается +(`DROP`)**, а не пересылается обратно. Максимум одна итерация пересылки на +пакет. + +### 6.5. Исключение локальных адресов + +Miss-правило применяется **только** к трафику, адресованному абонентам. Пакеты, +адресованные самим UPF, не должны уходить к пиру. Исключаются: + +- локальные N3/N9-адреса UPF; +- адреса управления (API, PFCP, Prometheus); +- адреса BGP/маршрутизации, если используются. + +Проверка «не локальный адрес» выполняется **до** принятия решения об +IPIP-пересылке. + +### 6.6. MTU и PMTU + +- **MTU:** IPIP добавляет **20 байт** внешнего IPv4-заголовка. MTU межядерного + канала должен быть ≥ MTU N6 + 20. +- **Ловушка PMTU:** при превышении MTU промежуточный маршрутизатор генерирует + ICMP Frag-Needed, но он приходит на UPF (источник внешнего IPIP-заголовка), + а не на исходного отправителя в DN. Ретрансля PMTU в DN не происходит + автоматически, и легитимные полноразмерные пакеты могут отбрасываться. + **Рекомендация:** поднять MTU межядерного канала ≥ N6 MTU + 20, чтобы + избежать фрагментации и зависимости от PMTU. + +### 6.7. Только unicast + +В miss-путь попадают также broadcast/multicast и потенциально вредоносный +сканирующий трафик. Для предотвращения рефлексии/амплификации IPIP-пересылка +выполняется **только для unicast-пакетов**. Broadcast/multicast на miss-пути +отбрасываются. + +### 6.8. Обработка устаревших/IDLE-сессий + +**Сценарий:** абонент выполнил re-attach в ядро B, но в ядре A осталась +устаревшая IDLE-сессия. SMF ядра A сохраняет downlink PDR (поиск по DST IP +находит сессию), но переводит FAR в режим NOCP (без FORW) — пересылка +запрещена, ожидается paging. + +**Поведение без фикса:** DL-пакет, хешированный на UPF-A (stale-сессия), +находит сессию, но FAR=NOCP → `XDP_DROP`. Пакет теряется, не пересылается +пиру. При этом срабатывает NOCP-триггер — SMF ядра A получает +DownlinkNotificationReport и пытается page UE (бесполезно, UE в ядре B). +Результат: ~50% DL-loss для affected-абонента + бессмысленный paging. + +**Поведение с фиксом:** если сессия найдена, но FAR не содержит FORW, +применяется `try_rebalance` — пакет пересылается пиру (UPF-B), где активная +сессия доставляет его абоненту. NOCP-триггер сохранён до rebalance, поэтому +SMF ядра A получает нотификацию как сигнал к очистке устаревшей сессии. +Anti-loop: redirected-пакет с non-FORW FAR → `XDP_DROP` (не уходит обратно). + +--- + +## 7. Плоскость данных (Вариант A) + +Изменений не требуется. UPF работает штатно: downlink ищется по DST IP в +`pdr_map_downlink_ip4/6`, при отсутствии — `XDP_PASS` (ядро ОС). Поскольку DN +маршрутизирует по префиксу, «чужого» downlink не возникает. Uplink — штатно по +TEID. + +--- + +## 8. Конфигурация + +### 8.1. Общие требования + +- **gNB:** два NG-контекста (к 5GC-стороне каждого ядра). Выбор ядра для UE — + на этапе регистрации (AMF selection, TAC/планирование). +- **Uplink:** gNB направляет по N3 TEID в якорное ядро — настройки UPF не + меняются. + +### 8.2. Вариант A + +- **Пулы IP:** непересекающиеся пулы для ядер A и B (напр. `10.60.0.0/25` → A, + `10.60.0.128/25` → B). Назначаются через SMF/UDM каждого ядра. +- **DN-маршрутизация:** статические маршруты или BGP: префикс пула A → UPF-A, + префикс пула B → UPF-B. ECMP не требуется. +- **UPF:** без изменений; стандартные поля `n3_address`, `n6` через + `interface_name` и т.д. + +### 8.3. Вариант B + +Конфигурация UPF расширяется (по стилю существующих полей в +`cmd/config/config.go`, напр. `n9_address`, `n9_adv_address`): + +| Поле | Назначение | +|---|---| +| `ipip_peer_address` | IP-адрес пира UPF для IPIP-туннеля (destination внешней инкапсуляции). | +| `ipip_local_address` | Локальный IP UPF на IPIP-связи (source внешней инкапсуляции). | +| `ipip_interface` | Идентификатор/имя интерфейса меж-ядерной связи (для anti-loop, 6.4). | + +Реализация поддерживает несколько абонентских префиксов (несколько APN): +список `ip4_ue_prefixes` (CLI `--ip4prefs`, повторяемый флаг) хранится в eBPF +в виде LPM-trie (`ue_rebalance_lpm_map`), один глобальный адрес пира +(`rebalance_address`/`rebalance_local_address`) применяется ко всем префиксам. + +Дополнительные требования к окружению: + +- **ECMP у DN:** строго **per-flow (5-tuple)**. Per-packet хеширование недопустимо + (изменение порядка пакетов). Допускается L3-only hash, если потоки UE + достаточно распределены по DST IP, но L3/L4-хеш предпочтителен. +- **Отсутствие NAT/PAT на N6:** DN видит реальный IP абонента (DST IP = IP UE). + Обязательное условие корректности miss-правила (поиск по DST IP). +- **Подсеть IPIP:** выделенная подсеть UPF-A ↔ UPF-B, изолированная от N3 и N6 + (не публикуется в DN). +- **Координация динамических IP:** непересекающиеся динамические субпулы внутри + общего пула или централизованное выделение — во избежание коллизий. + +Контрольная плоскость (PFCP/N4) остаётся в пределах якорного ядра — между ядрами +PDR/FAR/QER **не реплицируются**. + +--- + +## 9. Сценарии отказов и их обработка + +| # | Сценарий отказа | Вариант | Воздействие | Механизм восстановления | RTO | +|---|---|---|---|---|---| +| 1 | **Отказ UPF ядра A** (ядро живо, UPF down) | A | DL абонентов пула A теряется; uplink в «мёртвый» N3-туннель | gNB фиксирует отсутствие ответа N3 → re-attach UE в ядро Б, новый IP из пула Б | Секунды | +| 1 | **Отказ UPF ядра A** | B | DL абонентов A, хешированный на UPF-A, теряется; uplink в «мёртвый» N3 | Re-attach в ядро Б, **статический IP сохраняется** | Секунды | +| 2 | **Отказ всего ядра A** (AMF+SMF+UPF) | A/B | То же + потеря CP-контекста | Re-attach в ядро Б (5G AKA / EAP-AKA'); IP: A — из пула Б, B — сохраняется (статика) | Секунды | +| 3 | **Обрыв IPIP (1 из 2+ линков)** | B | Частичная деградация пропускной способности межядерного канала | ECMP/LACP перенаправляет на оставшийся линк | Миллисекунды | +| 4 | **Полный обрыв IPIP** | B | **Все** абоненты теряют ~50% DL (половина, хешированная на не-якорный UPF, не пересылается). Uplink не затронут | Восстановление линка; до этого TCP восстанавливается ретранссией, UDP страдает | Зависит от транспорта | +| 5 | **Обрыв DN-маршрута к UPF-A** | A | DL абонентов пула A полностью теряется | Восстановление маршрута; либо re-attach в Б (но IP из пула Б) | Секунды | +| 5 | **Обрыв DN-маршрута к UPF-A** | B | Весь DL перенаправляется на UPF-B → при чужих сессиях уходит через IPIP на UPF-A. **Деградация задержки**, сервис сохранён | Восстановление маршрута | Секунды | +| 6 | **Обрыв NG к одному ядру** | A/B | UE не могут регистрироваться в отказавшем ядре; существующие сессии этого ядра теряются | Re-attach через живое NG-подключение | Секунды | +| 7 | **Двойная сессия** (re-attach без cleanup старой IDLE) | B | DL на UPF со stale-сессией: FAR=NOCP → без фикса DROP (~50% loss + paging через старое ядро) | Rebalance при non-FORW FAR; NOCP-нотификация сохранена для cleanup | Мгновенно (rebalance) | + +> Примечание: во всех сценариях с отказом ядра восстановление **не бесшовное** — +> абонент проходит повторную регистрацию. Разница между вариантами — в +> сохранении IP: Вариант A выдаёт новый IP из пула живого ядра, Вариант B +> сохраняет статический IP (общий пул). + +--- + +## 10. Мониторинг + +Рекомендуемые метрики (Prometheus, в стиле `docs/metrics.md`) — в основном для +Варианта B: + +- `upf_ipip_forwarded_packets_total` — пакеты, пересланные пиру (miss на N6). +- `upf_ipip_received_packets_total` — пакеты, полученные по IPIP от пира. +- `upf_ipip_loop_dropped_total` — пакеты, отброшенные по anti-loop (miss на + IPIP-интерфейсе). +- `upf_ipip_local_dropped_total` — пакеты, исключённые из пересылки как + локальные/не-unicast. +- `upf_ipip_encap_errors_total` / `upf_ipip_decap_errors_total` — ошибки + инкапсуляции/декапсуляции. + +Интерпретация: + +- Высокое `upf_ipip_forwarded_packets_total` относительно общего DL — индикатор + дисбаланса ECMP или ошибки конфигурации (перекрывающиеся пулы, нарушение + координации динамики). +- `upf_ipip_loop_dropped_total > 0` — признак рассинхронизации таблиц PDR между + ядрами. + +Для Варианта A специфические метрики не требуются — используются стандартные +счётчики UPF (`upf_*`). + +--- + +## 11. Безопасность + +- **Доверенный транспорт (Вариант B).** Канал IPIP рассматривается внутри + доверенной среды передачи (выделенная линия/защищённый VLAN). **IPsec не + требуется** в базовой схеме; при необходимости добавляется на уровне + транспорта без изменения плоскости данных. +- **Анти-рефлексия.** Miss-путь пересылает только unicast; broadcast/multicast + не пересылаются, что снижает риск отражательных атак. +- **Анти-цикл.** Ограничение на одну итерацию пересылки (6.4) исключает + бесконечную циркуляцию пакетов. +- **Изоляция подсети IPIP.** Межядерный канал не публикуется в DN и не + пересекается с N3/N6. +- **Вариант A** — IPIP отсутствует, дополнительные меры безопасности не требуются. + +--- + +## 12. Ограничения и риски + +1. **Нет бесшовного failover ядра (оба варианта).** Схема — резервирование *пути* + downlink и горячий резерв ядра, а **не** репликация состояния сессий. При + отказе якоря — re-attach с перерывом и повторной аутентификацией. +2. **Сохранение IP (различие вариантов).** Вариант A не сохраняет IP при отказе + (новый из чужого пула). Вариант B сохраняет статические IP, но требует + координации динамических пулов во избежание коллизий. +3. **Стоимость Варианта B в нормальном режиме.** До ~50% downlink постоянно + проходит через межядерный канал; канал и оба UPF должны быть рассчитаны + на этот трафик. +4. **SPOF при одиночном IPIP-линке (Вариант B).** Требуется 2+ независимых линка + (ECMP/LACP); иначе полный обрыв связи = потеря ~50% DL для всех абонентов. +5. **Зависимость от ECMP у DN (Вариант B).** Корректность требует per-flow + хеширования и отсутствия NAT на N6. +6. **PMTU-ловушка (Вариант B).** ICMP Frag-Needed приходит на UPF, а не на + отправителя в DN — рекомендуется поднять MTU канала. +7. **Отсутствие 3GPP-стандарта** для постоянного меж-UPF «miss-forwarding» — + решение кастомное, требует поддержки в eBPF-двигателе UPF (Вариант B). + +--- + +## 13. Рекомендация по выбору варианта + +- **Динамические абоненты без требования сохранить IP** → **Вариант A**. + Проще, надёжнее, нулевой межядерный трафик, без изменений в UPF. +- **Статически назначенные IP / требование сохранить IP при отказе ядра** → + **Вариант B**. Общий пул, ECMP + IPIP miss-forward. +- **Гибрид:** динамические абоненты — по Варианту A (разделённые субпулы, + prefix-routing), статические — по Варианту B (общий статический диапазон, + ECMP + IPIP). Требует, чтобы DN комбинировал prefix-routing для динамики и + ECMP для статики; реализуемо при поддержке со стороны маршрутизации DN. From 661f1f94c6cb71ee7dd4b5258cb633230bc31d1a Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Thu, 22 Oct 2026 13:33:02 +0300 Subject: [PATCH 17/18] docs(config): document downlink rebalance parameters Add ip4_ue_prefixes, rebalance_address and rebalance_local_address to the configuration reference table in docs/Configuration.md. --- docs/Configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index 69aa3248..3dba02e7 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -50,6 +50,9 @@ Trace packets in file `Optional` | Maximum number of packets in one Max trace size `Optional` | Maximum size (in bytes) of one trace dump file | `trace_max_size` | UPF_TRACE_MAX_SIZE | --tracefsize int | 10485760 IPv6 router advertisement `Optional` | Enable or disable IPv6 Router Advertisement support | `ip6_ra_support` | UPF_IP6_RA_SUPPORT | --ip6ra | FALSE IPv6 subscriber prefix `Optional` | Subscriber IPv6 prefix | `ip6_ra_prefix` | UPF_IP6_RA_PREFIX | --ip6rapref string | 2a03:d000:29a0:509::/64 +IPv4 UE prefixes `Optional` | Subscriber IPv4 prefixes eligible for downlink rebalancing. Each entry is an IPv4 CIDR. Stored in an LPM trie (max 16 entries). | `ip4_ue_prefixes` | UPF_IP4_UE_PREFIXES | --ip4prefs stringArray | - +Downlink rebalance address `Optional` | Peer UPF address for downlink rebalancing (IPIP tunnel destination). Requires at least one `ip4_ue_prefixes` entry. | `rebalance_address` | UPF_REBALANCE_ADDRESS | --rbaddr string | - +Downlink rebalance local address `Optional` | Local source address for downlink rebalancing (IPIP tunnel source). | `rebalance_local_address` | UPF_REBALANCE_LOCAL_ADDRESS | --rbladdr string | - We are using [Viper](https://github.com/spf13/viper) for configuration handling, [Viper](https://github.com/spf13/viper) uses the following precedence order. Each item takes precedence over the item below it: From de91250fb47efd54fd848cbeefedd2d5209d7bd2 Mon Sep 17 00:00:00 2001 From: pirog-spb Date: Mon, 26 Oct 2026 17:14:25 +0300 Subject: [PATCH 18/18] fix(ebpf): set BPF_F_NO_PREALLOC on ue_rebalance_lpm_map The kernel rejects LPM trie maps created without BPF_F_NO_PREALLOC, so ue_rebalance_lpm_map creation failed with EINVAL. Declare the flag in the BTF map definition; cilium/ebpf does not set it automatically. --- cmd/ebpf/xdp/rebalance.h | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/ebpf/xdp/rebalance.h b/cmd/ebpf/xdp/rebalance.h index 2a0f4565..98a0f620 100644 --- a/cmd/ebpf/xdp/rebalance.h +++ b/cmd/ebpf/xdp/rebalance.h @@ -32,4 +32,5 @@ struct __type(key, struct ue_rebalance_lpm_key); __type(value, __u8); __uint(max_entries, 16); + __uint(map_flags, BPF_F_NO_PREALLOC); } ue_rebalance_lpm_map SEC(".maps");