Skip to content

Commit f87971f

Browse files
Fei Chenmeta-codesync[bot]
authored andcommitted
Add CIDR prefix VIP support in BPF datapath
Summary: Add vip_lpm_map (BPF_MAP_TYPE_LPM_TRIE) for prefix-based VIP lookup, gated behind CIDR_VIP_LOOKUP macro. When a packet's destination IPv6 address misses exact-match vip_map, the datapath falls back to LPM trie lookup before trying port=0 wildcard. - balancer_consts.h: MAX_LPM_VIPS, LPM_VIP_CNTR - balancer_maps.h: vip_lpm_map definition - balancer.bpf.c: LPM fallback in process_packet() - BUCK: cidr_vip_lookup_gue BPF flavor - Test fixtures and provision for CIDR VIP BPF tests Reviewed By: avasylev Differential Revision: D100089958 fbshipit-source-id: c218abc9006a593beb2d3bd09002469151922a7b
1 parent 107c646 commit f87971f

10 files changed

Lines changed: 283 additions & 4 deletions

File tree

katran/lib/KatranLb.cpp

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,13 @@ bool KatranLb::addVip(const VipKey& vip, const uint32_t flags) {
10211021
LOG(ERROR) << "Invalid Vip address: " << vip.address;
10221022
return false;
10231023
}
1024+
if (vip.cidrVipPrefixLen > 0) {
1025+
if (!folly::IPAddress::validate(vip.address) ||
1026+
!folly::IPAddress(vip.address).isV6()) {
1027+
LOG(ERROR) << "CIDR VIP only supported for IPv6, got: " << vip.address;
1028+
return false;
1029+
}
1030+
}
10241031
VLOG(1) << fmt::format(
10251032
"adding new vip: {}:{}:{}", vip.address, vip.port, vip.proto);
10261033

@@ -1040,7 +1047,11 @@ bool KatranLb::addVip(const VipKey& vip, const uint32_t flags) {
10401047
vip_meta meta;
10411048
meta.vip_num = vip_num;
10421049
meta.flags = flags;
1043-
updateVipMap(ModifyAction::ADD, vip, &meta);
1050+
if (vip.cidrVipPrefixLen > 0) {
1051+
updateVipLpmMap(ModifyAction::ADD, vip, &meta);
1052+
} else {
1053+
updateVipMap(ModifyAction::ADD, vip, &meta);
1054+
}
10441055
}
10451056
return true;
10461057
}
@@ -1102,7 +1113,13 @@ bool KatranLb::delVip(const VipKey& vip) {
11021113
}
11031114
vipNums_.push_back(vip_iter->second.getVipNum());
11041115
if (!config_.testing) {
1105-
updateVipMap(ModifyAction::DEL, vip);
1116+
// Use the stored key which has cidrVipPrefixLen from addVip
1117+
const auto& storedVipKey = vip_iter->first;
1118+
if (storedVipKey.cidrVipPrefixLen > 0) {
1119+
updateVipLpmMap(ModifyAction::DEL, storedVipKey);
1120+
} else {
1121+
updateVipMap(ModifyAction::DEL, storedVipKey);
1122+
}
11061123
}
11071124
vips_.erase(vip_iter);
11081125
return true;
@@ -1169,7 +1186,11 @@ bool KatranLb::modifyVip(const VipKey& vip, uint32_t flag, bool set) {
11691186
vip_meta meta;
11701187
meta.vip_num = vip_iter->second.getVipNum();
11711188
meta.flags = vip_iter->second.getVipFlags();
1172-
return updateVipMap(ModifyAction::ADD, vip, &meta);
1189+
const auto& storedVipKey = vip_iter->first;
1190+
if (storedVipKey.cidrVipPrefixLen > 0) {
1191+
return updateVipLpmMap(ModifyAction::ADD, storedVipKey, &meta);
1192+
}
1193+
return updateVipMap(ModifyAction::ADD, storedVipKey, &meta);
11731194
}
11741195
return true;
11751196
}
@@ -3078,6 +3099,42 @@ bool KatranLb::updateVipMap(
30783099
return true;
30793100
}
30803101

3102+
bool KatranLb::updateVipLpmMap(
3103+
const ModifyAction action,
3104+
const VipKey& vip,
3105+
vip_meta* meta) {
3106+
auto vip_addr = IpHelpers::parseAddrToBe(vip.address);
3107+
if ((vip_addr.flags & V6DADDR) == 0) {
3108+
LOG(ERROR) << "LPM VIP only supported for IPv6, got: " << vip.address;
3109+
lbStats_.bpfFailedCalls++;
3110+
return false;
3111+
}
3112+
v6_lpm_key lpm_key = {};
3113+
lpm_key.prefixlen = vip.cidrVipPrefixLen;
3114+
std::memcpy(lpm_key.addr, vip_addr.v6daddr, 16);
3115+
3116+
if (action == ModifyAction::ADD) {
3117+
auto res = bpfAdapter_->bpfUpdateMap(
3118+
bpfAdapter_->getMapFdByName(KatranLbMaps::vip_lpm_map), &lpm_key, meta);
3119+
if (res != 0) {
3120+
LOG(ERROR) << "can't add element to vip_lpm_map, error: "
3121+
<< folly::errnoStr(errno);
3122+
lbStats_.bpfFailedCalls++;
3123+
return false;
3124+
}
3125+
} else {
3126+
auto res = bpfAdapter_->bpfMapDeleteElement(
3127+
bpfAdapter_->getMapFdByName(KatranLbMaps::vip_lpm_map), &lpm_key);
3128+
if (res != 0) {
3129+
LOG(ERROR) << "can't delete element from vip_lpm_map, error: "
3130+
<< folly::errnoStr(errno);
3131+
lbStats_.bpfFailedCalls++;
3132+
return false;
3133+
}
3134+
}
3135+
return true;
3136+
}
3137+
30813138
bool KatranLb::updateHcKeyMap(
30823139
const ModifyAction action,
30833140
const VipKey& hcKey,

katran/lib/KatranLb.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ constexpr auto per_hckey_stats = "per_hckey_stats";
135135
constexpr auto reals = "reals";
136136
constexpr auto server_id_map = "server_id_map";
137137
constexpr auto stats = "stats";
138+
constexpr auto vip_lpm_map = "vip_lpm_map";
138139
constexpr auto vip_map = "vip_map";
139140
constexpr auto vip_miss_stats = "vip_miss_stats";
140141
constexpr auto vip_to_down_reals_map = "vip_to_down_reals_map";
@@ -1131,6 +1132,14 @@ class KatranLb {
11311132
const VipKey& vip,
11321133
vip_meta* meta = nullptr);
11331134

1135+
/**
1136+
* update vip_lpm_map (add or remove prefix VIP) in forwarding plane
1137+
*/
1138+
bool updateVipLpmMap(
1139+
const ModifyAction action,
1140+
const VipKey& vip,
1141+
vip_meta* meta = nullptr);
1142+
11341143
/**
11351144
* Update hc_key_map (add or delete) in healthchecking bpf program.
11361145
*/

katran/lib/KatranLbStructs.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@ class VipKey {
325325
std::string address;
326326
uint16_t port;
327327
uint8_t proto;
328+
// 0 = exact-match VIP (default). >0 = CIDR prefix VIP (e.g., 96 for /96).
329+
// IPv6 only today. Must be greater than min_v6_prefix (currently 64).
330+
// Not included in hash/equality — (address, port, proto) uniquely identifies
331+
// a VIP. cidrVipPrefixLen is metadata carried alongside.
332+
uint8_t cidrVipPrefixLen{0};
328333

329334
bool operator==(const VipKey& other) const {
330335
return (

katran/lib/bpf/balancer.bpf.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,19 @@ process_packet(struct xdp_md* xdp, __u64 nh_off, bool is_ipv6) {
826826
vip.port = 0;
827827
vip_info = bpf_map_lookup_elem(&vip_map, &vip);
828828
if (!vip_info) {
829-
return XDP_PASS;
829+
#ifdef CIDR_VIP
830+
// Fallback to LPM prefix-based VIP lookup (IPv6 only).
831+
// LPM key has no port/proto, so one lookup is sufficient.
832+
if (is_ipv6) {
833+
struct v6_lpm_key lpm_key = {};
834+
lpm_key.prefixlen = 128;
835+
memcpy(lpm_key.addr, pckt.flow.dstv6, 16);
836+
vip_info = bpf_map_lookup_elem(&vip_lpm_map, &lpm_key);
837+
}
838+
#endif // CIDR_VIP
839+
if (!vip_info) {
840+
return XDP_PASS;
841+
}
830842
}
831843

832844
if (!(vip_info->flags & F_HASH_DPORT_ONLY) &&

katran/lib/bpf/balancer_consts.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@
5454
#define MAX_VIPS 512
5555
#endif
5656

57+
#ifndef MAX_CIDR_VIPS
58+
// MAX_CIDR_VIPS is the size of lpm trie map we use to sore meta info of cidr
59+
// vips while MAX_VIPS is used as the size of the regular vip_map for non-cidr
60+
// vips.
61+
// For now, we still use MAX_VIPS for the total max number of vips
62+
// including both non-cidr and cidr vips. CH_RING and stats maps are
63+
// still using MAX_VIPS as the max size used by both cidr and non-cidr vips
64+
// meanwhile.
65+
// TODO: we need a separate macro for the size of non-cidr vips to avoid
66+
// the confusion while MAX_VIP is the sum of non-cidr and cidr vips.
67+
#define MAX_CIDR_VIPS 256
68+
#endif
69+
5770
// 1 real is equal to 1 ip address. so server w/ v4 and v6 would most likely
5871
// consume 2 spots
5972
#ifndef MAX_REALS
@@ -297,6 +310,7 @@ v2 tracks misses for TCP non syns */
297310
#define XDP_PASS_CNTR 19 // packets passed up to the kernel
298311
// Tracks successful egress decap packets (XDP_TX after GUE decap)
299312
#define EGRESS_DECAP_CNTR 20
313+
#define LPM_VIP_CNTR 21 // packets matched via LPM prefix VIP lookup
300314

301315
// indice for all stats maps defined above correspond to entries in the map
302316
// stats starting from the index MAX_VIPS. The max_entries of stats is

katran/lib/bpf/balancer_maps.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ struct {
3737
__uint(map_flags, NO_FLAGS);
3838
} vip_map SEC(".maps");
3939

40+
#ifdef CIDR_VIP
41+
// map for prefix-based VIP lookup (IPv6 /96 prefix VIPs).
42+
// packets whose destination IPv6 address matches a prefix in this map
43+
// are load-balanced the same as exact-match VIPs.
44+
struct {
45+
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
46+
__type(key, struct v6_lpm_key);
47+
__type(value, struct vip_meta);
48+
__uint(max_entries, MAX_CIDR_VIPS);
49+
__uint(map_flags, BPF_F_NO_PREALLOC);
50+
} vip_lpm_map SEC(".maps");
51+
#endif // CIDR_VIP
52+
4053
// fallback lru. we should never hit this one outside of unittests
4154
struct {
4255
__uint(type, BPF_MAP_TYPE_LRU_HASH);
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// clang-format off
2+
3+
/* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation; version 2 of the License.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License along
15+
* with this program; if not, write to the Free Software Foundation, Inc.,
16+
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17+
*/
18+
19+
#pragma once
20+
#include <string>
21+
#include <vector>
22+
#include "katran/lib/testing/tools/PacketAttributes.h"
23+
#include "katran/lib/testing/tools/PacketBuilder.h"
24+
25+
namespace katran {
26+
namespace testing {
27+
/**
28+
* Test fixtures for CIDR_VIP feature.
29+
*
30+
* This feature enables prefix-based VIP matching using an LPM trie. When a
31+
* packet's destination IPv6 address does not match any exact VIP in vip_map,
32+
* the datapath falls back to vip_lpm_map for prefix-based matching.
33+
*
34+
* Setup: A /96 CIDR VIP is added at fc00:1::/96 with reals fc00::1 and fc00::2.
35+
* Packets to any address in fc00:1::0/96 (e.g., fc00:1::100, fc00:1::200)
36+
* should match the CIDR VIP and be load-balanced to a real.
37+
*
38+
* Test cases cover:
39+
* - IPv6 TCP packet matching CIDR VIP prefix (different IPs in /96 range)
40+
* - IPv6 UDP packet matching CIDR VIP prefix
41+
* - IPv6 packet NOT matching any prefix (XDP_PASS)
42+
* - IPv4 packet should not match CIDR VIP (IPv6 only)
43+
*
44+
* Note: These fixtures use GUE encapsulation.
45+
*/
46+
const std::vector<::katran::PacketAttributes> cidrVipTestFixtures = {
47+
// 1
48+
// ipv6 TCP: dst fc00:1::100 matches CIDR VIP fc00:1::/96 (port=0 means
49+
// all ports). Routes to real fc00::1 via GUE encap.
50+
{.description = "ipv6 tcp: CIDR VIP match. CIDR_VIP is required",
51+
.expectedReturnValue = "XDP_TX",
52+
.inputPacketBuilder = katran::testing::PacketBuilder::newPacket()
53+
.Eth("0x1", "0x2")
54+
.IPv6("fc00:2::1", "fc00:1::100")
55+
.TCP(31337, 80, 0, 0, 8192, TH_ACK)
56+
.payload("katran test pkt"),
57+
.expectedOutputPacketBuilder = katran::testing::PacketBuilder::newPacket()
58+
.Eth("02:00:00:00:00:00", "00:00:de:ad:be:af")
59+
.IPv6("fc00:2307::1337", "fc00::1", 64, 0, 0)
60+
.UDP(31337, 9886)
61+
.IPv6("fc00:2::1", "fc00:1::100")
62+
.TCP(31337, 80, 0, 0, 8192, TH_ACK)
63+
.payload("katran test pkt")
64+
},
65+
// 2
66+
// ipv6 TCP: dst fc00:1::200 also matches CIDR VIP fc00:1::/96 (port=0
67+
// means all ports). Different dest IP in same /96, should match same VIP.
68+
{.description = "ipv6 tcp: CIDR VIP match different IP in range. CIDR_VIP is required",
69+
.expectedReturnValue = "XDP_TX",
70+
.inputPacketBuilder = katran::testing::PacketBuilder::newPacket()
71+
.Eth("0x1", "0x2")
72+
.IPv6("fc00:2::1", "fc00:1::200")
73+
.TCP(31337, 443, 0, 0, 8192, TH_ACK)
74+
.payload("katran test pkt"),
75+
.expectedOutputPacketBuilder = katran::testing::PacketBuilder::newPacket()
76+
.Eth("02:00:00:00:00:00", "00:00:de:ad:be:af")
77+
.IPv6("fc00:2307::1337", "fc00::1", 64, 0, 0)
78+
.UDP(31337, 9886)
79+
.IPv6("fc00:2::1", "fc00:1::200")
80+
.TCP(31337, 443, 0, 0, 8192, TH_ACK)
81+
.payload("katran test pkt")
82+
},
83+
// 3
84+
// ipv6 TCP: dst fc00:1::400 with a different port (8080) matches
85+
// CIDR VIP fc00:1::/96 (port=0 means all ports).
86+
// Packet misses vip_map (exact port), misses vip_map (port=0),
87+
// then hits LPM trie.
88+
{.description = "ipv6 tcp: CIDR VIP match on different port. CIDR_VIP is required",
89+
.expectedReturnValue = "XDP_TX",
90+
.inputPacketBuilder = katran::testing::PacketBuilder::newPacket()
91+
.Eth("0x1", "0x2")
92+
.IPv6("fc00:2::1", "fc00:1::400")
93+
.TCP(31337, 8080, 0, 0, 8192, TH_ACK)
94+
.payload("katran test pkt"),
95+
.expectedOutputPacketBuilder = katran::testing::PacketBuilder::newPacket()
96+
.Eth("02:00:00:00:00:00", "00:00:de:ad:be:af")
97+
.IPv6("fc00:2307::1337", "fc00::1", 64, 0, 0)
98+
.UDP(31337, 9886)
99+
.IPv6("fc00:2::1", "fc00:1::400")
100+
.TCP(31337, 8080, 0, 0, 8192, TH_ACK)
101+
.payload("katran test pkt")
102+
},
103+
// 4
104+
// ipv6: dst fc00:3::1 does NOT match CIDR VIP fc00:1::/96 (port=0 means
105+
// all ports, but prefix doesn't match). Should be passed to kernel stack.
106+
{.description = "ipv6: CIDR VIP miss. CIDR_VIP is required",
107+
.expectedReturnValue = "XDP_PASS",
108+
.inputPacketBuilder = katran::testing::PacketBuilder::newPacket()
109+
.Eth("0x1", "0x2")
110+
.IPv6("fc00:2::1", "fc00:3::1")
111+
.TCP(31337, 80, 0, 0, 8192, TH_ACK)
112+
.payload("katran test pkt"),
113+
.expectedOutputPacketBuilder = katran::testing::PacketBuilder::newPacket()
114+
.Eth("0x1", "0x2")
115+
.IPv6("fc00:2::1", "fc00:3::1")
116+
.TCP(31337, 80, 0, 0, 8192, TH_ACK)
117+
.payload("katran test pkt")
118+
},
119+
};
120+
121+
} // namespace testing
122+
} // namespace katran

katran/lib/testing/utils/KatranTestProvision.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ void prepareOptionalLbData(katran::KatranLb& lb) {
229229
lb.modifyReal("10.0.0.6", kLocalReal);
230230
}
231231

232+
void prepareCidrVipLbData(katran::KatranLb& lb) {
233+
katran::VipKey vip;
234+
// adding a /96 CIDR VIP for testing prefix-based VIP matching
235+
vip.address = "fc00:1::";
236+
vip.port = 0;
237+
vip.proto = kTcp;
238+
vip.cidrVipPrefixLen = 96;
239+
lb.addVip(vip);
240+
addReals(lb, vip, {"fc00::1", "fc00::2"});
241+
}
242+
232243
void prepareLbDataStableRt(katran::KatranLb& lb) {
233244
lb.restartKatranMonitor(kMonitorLimit);
234245
katran::VipKey vip;

katran/lib/testing/utils/KatranTestProvision.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ void prepareLbData(katran::KatranLb& lb, bool skipLru = false);
180180

181181
void prepareOptionalLbData(katran::KatranLb& lb);
182182

183+
void prepareCidrVipLbData(katran::KatranLb& lb);
184+
183185
void prepareLbDataStableRt(katran::KatranLb& lb);
184186

185187
void prepareLbDataXpopDecap(katran::KatranLb& lb);

katran/lib/tests/KatranLbTest.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,40 @@ TEST_F(KatranLbTest, testAddingInvalidVip) {
159159
ASSERT_FALSE(lb->addVip(v));
160160
};
161161

162+
TEST_F(KatranLbTest, testCidrVipAddDelete) {
163+
VipKey v;
164+
v.address = "fc00::1";
165+
v.port = 0;
166+
v.proto = 6;
167+
v.cidrVipPrefixLen = 96;
168+
// adding and deleting a CIDR VIP
169+
ASSERT_TRUE(lb->addVip(v));
170+
ASSERT_TRUE(lb->delVip(v));
171+
};
172+
173+
TEST_F(KatranLbTest, testCidrVipInvalidIpv4) {
174+
VipKey v;
175+
v.address = "192.168.1.1";
176+
v.port = 0;
177+
v.proto = 6;
178+
v.cidrVipPrefixLen = 96;
179+
// CIDR VIP with IPv4 address should fail
180+
ASSERT_FALSE(lb->addVip(v));
181+
};
182+
183+
TEST_F(KatranLbTest, testCidrVipModify) {
184+
VipKey v;
185+
v.address = "fc00::1";
186+
v.port = 0;
187+
v.proto = 6;
188+
v.cidrVipPrefixLen = 96;
189+
ASSERT_TRUE(lb->addVip(v));
190+
// modifying flags on a CIDR VIP
191+
ASSERT_TRUE(lb->modifyVip(v, 1, true));
192+
ASSERT_EQ(lb->getVipFlags(v), 1);
193+
ASSERT_TRUE(lb->delVip(v));
194+
};
195+
162196
TEST_F(KatranLbTest, testRealHelpers) {
163197
lb->addVip(v1);
164198

0 commit comments

Comments
 (0)