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/config/config.go b/cmd/config/config.go index 337b9352..183af0f7 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -113,6 +113,9 @@ 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"` + 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"` n3AdvertisedAddress5G net.IP } @@ -183,6 +186,9 @@ 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.StringArray("ip4prefs", []string{}, "Subscriber IPv4 prefixes") + pflag.String("rbaddr", "", "Downlink rebalace address") + pflag.String("rbladdr", "", "Downlink rebalace local address") pflag.Parse() } @@ -264,6 +270,9 @@ 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_prefixes", pflag.Lookup("ip4prefs")) + _ = 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/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) { diff --git a/cmd/ebpf/objects.go b/cmd/ebpf/objects.go index b65c1003..e3905ae9 100644 --- a/cmd/ebpf/objects.go +++ b/cmd/ebpf/objects.go @@ -86,8 +86,9 @@ 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, + "ue_rebalance_lpm_map": 16, } replacements := make(map[string]*ebpf.Map) @@ -127,6 +128,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 } @@ -251,7 +261,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..756dbc1e 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) } @@ -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/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/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/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 dc85a15d..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" @@ -67,6 +68,8 @@ struct dataplane_config { __u8 trace_in; __u8 trace_out; __u8 trace_blocked; + __u32 rebalance_local_ip; + __u32 rebalance_ip; __u8 ip6_ra_support; __u128 ip6_ra_prefix; __u16 ip6_ra_prefix_length; @@ -202,11 +205,97 @@ 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, __u32 saddr, __u32 daddr) { + + //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; + } + // 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) - 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 *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; + + __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 + sizeof(struct iphdr)); + 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 int try_rebalance(struct packet_context *ctx) { + 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) { + 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); if (!session) { upf_printk("upf: [n6] no downlink session for ip:%pI4", &ip4->daddr); + + if(ctx->redirected) { + upf_printk("upf: [n6] drop redirected packet: no session for ip:%pI4", &ip4->daddr); + return XDP_DROP; + } + + int rebalance_action = try_rebalance(ctx); + if(rebalance_action != -1) + return rebalance_action; return DEFAULT_XDP_ACTION; } @@ -239,8 +328,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)) @@ -353,7 +450,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; @@ -532,6 +629,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); @@ -549,6 +650,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 +678,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 +751,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"; 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, diff --git a/cmd/ebpf/xdp/rebalance.h b/cmd/ebpf/xdp/rebalance.h new file mode 100644 index 00000000..98a0f620 --- /dev/null +++ b/cmd/ebpf/xdp/rebalance.h @@ -0,0 +1,36 @@ +/** + * 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); + __uint(map_flags, BPF_F_NO_PREALLOC); +} ue_rebalance_lpm_map SEC(".maps"); 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; 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; }; 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..20ceb3b5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -143,6 +143,31 @@ func main() { entrypointConfig.Ip6RaPrefixLength = uint16(ip6PrefixLength) } + if config.Conf.DLRebalanceAddress != "" { + 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 != "" { + 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) + } + + 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) + } + } + } + if err := bpfObjects.GlobalConfig.Set(entrypointConfig); err != nil { log.Fatal().Err(err).Msgf("can't set dataplane global config") } 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: 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.