From 73965a658583314a8ae8de0390b1644196d224ba Mon Sep 17 00:00:00 2001 From: Peng Yang <707042815@qq.com> Date: Sat, 22 Oct 2022 02:46:33 +0800 Subject: [PATCH] Add XDP hardware offload support for certain Mizar functions with Netronome NIC (#658) * easy shell for ustc * load transit_xdp1 with offload mode * update offloaded endpoint_map * add trn_transit_xdp1 with 3 layers function * replace xdp1 with xdp1_3layers and add related offload_map fd * update easy shell for ustc * update offloaded network_map * test for performance * test for vpc and modify num of ep&net * fix for merging * load offload XDP with double check * fix for ustc * fix for ustc * fix * reset * remove unnecessary file * Remove easy shell for USTC * Remove easy shell for USTC * fix for merging * Fix for PR * Fix for USTC * Fix update_vpc/net/ep unittest * Fix the code format and add some err handlers. * Remove offload CLI and Restrict offload NIC yaml * Remove offload CLI * Update original unittest in generic mode * Add unittest in offload mode * Fix log description err. * Fix the code style and format. * Remove changes of get_default_itf * Update for code style and format * Add rc to the log * Fix some errors * Fix offload codes * Fix offload codes * Remove blank line * Update docs * Update docs * Update * Update * Update --- .../offload_XDP/design_and_limitations.md | 30 ++ .../offload_XDP/offload_XDP_workflow.png | Bin 0 -> 61473 bytes etc/deploy/deploy.mizar.dev.yaml | 2 + mizar/common/common.py | 30 ++ mizar/daemon/app.py | 23 + src/dmn/test/test_dmn.c | 218 ++++++++ src/dmn/trn_rpc_protocol_handlers_1.c | 100 ++-- src/dmn/trn_transit_xdp_usr.c | 158 ++++++ src/dmn/trn_transit_xdp_usr.h | 15 + src/include/trn_datamodel.h | 23 + src/xdp/trn_transit_xdp_hardware_offload.c | 506 ++++++++++++++++++ supported_xdp_offload_nics.yaml | 5 + 12 files changed, 1070 insertions(+), 40 deletions(-) create mode 100644 docs/design/features/offload_XDP/design_and_limitations.md create mode 100644 docs/design/features/offload_XDP/offload_XDP_workflow.png create mode 100644 src/xdp/trn_transit_xdp_hardware_offload.c create mode 100644 supported_xdp_offload_nics.yaml diff --git a/docs/design/features/offload_XDP/design_and_limitations.md b/docs/design/features/offload_XDP/design_and_limitations.md new file mode 100644 index 00000000..891926f5 --- /dev/null +++ b/docs/design/features/offload_XDP/design_and_limitations.md @@ -0,0 +1,30 @@ + +Netronome SmartNIC has following limitations: + +- Only supports maps of type BPF_MAP_TYPE_ARRAY/BPF_MAP_TYPE_HASH. +- Constraints on the size of the maps: + - Sum of map entries on the NIC should be less than 3,072,000; + - A maximum limit of 64 bytes per entry. +- Constraints on the size of the program: the maximum number of instructions on the SmartNIC is only 2800. +- Does not support XDP_REDIRECT. + + + +Considering the limitations above, bouncers and dividers (forwarding function) are offloaded to the NIC. + + + +The first packet between two endpoints will pass through the bouncer/divider. This forwarding function will be implemented on the NIC. On the contrary, for the XDP program on the endpoint host's kernel, every packet (not only the first) will pass through it. The main function is to redirect rather than forward, so it is not the offloaded target. + + + +The offloaded workflow is as follows (The black line represents the logic of the mizar, and the red line represents the partial function offloaded to SmartNIC): + +![workflow of offloading XDP](offload_XDP_workflow.png) \ No newline at end of file diff --git a/docs/design/features/offload_XDP/offload_XDP_workflow.png b/docs/design/features/offload_XDP/offload_XDP_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..bf1ada5e07c9ce5e463b1335ce614c99430cefd4 GIT binary patch literal 61473 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μT;XgLpO&}saSAPd5ZkOiPyU=W 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 literal 0 HcmV?d00001 diff --git a/etc/deploy/deploy.mizar.dev.yaml b/etc/deploy/deploy.mizar.dev.yaml index df3fa1d7..649ce740 100644 --- a/etc/deploy/deploy.mizar.dev.yaml +++ b/etc/deploy/deploy.mizar.dev.yaml @@ -527,6 +527,8 @@ spec: value: 'false' - name: FEATUREGATE_BWQOS value: 'false' + - name: FEATUREGATE_OFFLOAD_XDP + value: 'false' securityContext: privileged: true volumes: diff --git a/mizar/common/common.py b/mizar/common/common.py index c31c928f..f367924f 100644 --- a/mizar/common/common.py +++ b/mizar/common/common.py @@ -28,6 +28,7 @@ import datetime import json import dateutil.parser +import yaml from kubernetes import watch, client, config from kubernetes.client.rest import ApiException from ctypes.util import find_library @@ -434,6 +435,35 @@ def conf_list_has_max_elements(conf, conf_list): return True return False +def supported_offload_xdp_itf_names(): + """ + According to the list of NIC names, corresponding logic interface names are returned. + """ + logical_itf_names = [] + with open("/var/mizar/supported_xdp_offload_nics.yaml", "r", encoding="utf-8") as f: + supported_nic_names = yaml.load(f, Loader=yaml.FullLoader) + + rc, data = run_cmd("lspci -mm | grep 'Ethernet controller'") + if rc is not None: + logging.info("Failure running \"lspci -mm | grep 'Ethernet controller'\" with rc:" + f'''{rc}''') + return logical_itf_names + + eth_crtls = [i for i in data.split('\n') if i] + for eth_crtl in eth_crtls: + for vendor_name in supported_nic_names.keys(): + for model_name in supported_nic_names[vendor_name]: + if vendor_name.lower() in eth_crtl.lower() and model_name.lower() in eth_crtl.lower(): + pci_num = eth_crtl.split()[0] + rc, data = run_cmd("ls -l /sys/class/net | grep %s" % pci_num) + if rc is not None: + continue + else: + logical_itfs = [i for i in data.split('\n') if i] + for logical_itf in logical_itfs: + logical_itf_names.append(logical_itf.split('/')[-1]) + + return logical_itf_names + def get_default_itf(): """ Assuming "ip route" returns the following format: diff --git a/mizar/daemon/app.py b/mizar/daemon/app.py index 5868b09c..8fdd23be 100644 --- a/mizar/daemon/app.py +++ b/mizar/daemon/app.py @@ -94,6 +94,11 @@ def init(benchmark=False): output = r.stdout.read().decode().strip() logging.info("Removed existing XDP program: {}".format(output)) + cmd = "nsenter -t 1 -m -u -n -i ip link set dev " + f'''{default_itf}''' + " xdpoffload off" + r = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + output = r.stdout.read().decode().strip() + logging.info("Removed existing offload XDP program: {}".format(output)) + cmd = "nsenter -t 1 -m -u -n -i /trn_bin/transitd >transitd.log &" r = subprocess.Popen(cmd, shell=True) logging.info("Running transitd") @@ -119,6 +124,24 @@ def init(benchmark=False): output = r.stdout.read().decode().strip() logging.info("Running load-transit-xdp: {}".format(output)) + # Offload XDP program removes codes about debugging for size limitation. + if os.getenv('FEATUREGATE_OFFLOAD_XDP', 'false').lower() in ('true', '1'): + if default_itf in supported_offload_xdp_itf_names(): + config = { + "xdp_path": "/trn_xdp/trn_transit_xdp_hardware_offload_ebpf.o", + "pcapfile": "/bpffs/transit_xdp_offload.pcap", + "xdp_flag": CONSTANTS.XDP_OFFLOAD + } + config = json.dumps(config) + cmd = (f'''nsenter -t 1 -m -u -n -i /trn_bin/transit -s {nodeip} load-transit-xdp -i {default_itf} -j '{config}' ''') + r = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + output = r.stdout.read().decode().strip() + logging.info("Running load-transit-xdp with offload mode: {}".format(output)) + else: + logging.info("Offloading transit XDP functionality not supported for interface {}".format(default_itf)) + else: + logging.info("Offload XDP feature is disabled.") + if os.getenv('FEATUREGATE_BWQOS', 'false').lower() in ('false', '0'): logging.info("Bandwidth QoS feature is disabled.") return diff --git a/src/dmn/test/test_dmn.c b/src/dmn/test/test_dmn.c index 78ee6851..627d9f96 100644 --- a/src/dmn/test/test_dmn.c +++ b/src/dmn/test/test_dmn.c @@ -400,6 +400,7 @@ static void do_lo_xdp_load(void) xdp_intf.interface = itf; xdp_intf.xdp_path = xdp_path; xdp_intf.pcapfile = pcapfile; + xdp_intf.xdp_flag = XDP_FLAGS_SKB_MODE; int *rc; expect_function_call(__wrap_bpf_map_update_elem); @@ -408,6 +409,23 @@ static void do_lo_xdp_load(void) assert_int_equal(*rc, 0); } +static void do_lo_offload_xdp_load(void) +{ + rpc_trn_xdp_intf_t xdp_intf; + char itf[] = "lo"; + char xdp_path[] = "/path/to/xdp/object/file"; + char pcapfile[] = "/path/to/bpf/pinned/map"; + xdp_intf.interface = itf; + xdp_intf.xdp_path = xdp_path; + xdp_intf.pcapfile = pcapfile; + xdp_intf.xdp_flag = XDP_FLAGS_HW_MODE; + + int *rc; + expect_function_call(__wrap_bpf_map_update_elem); + rc = load_transit_xdp_1_svc(&xdp_intf, NULL); + assert_int_equal(*rc, 0); +} + static void do_lo_xdp_unload(void) { int *rc; @@ -466,6 +484,27 @@ static void test_update_vpc_1_svc(void **state) assert_int_equal(*rc, 0); } +static void test_update_offload_vpc_1_svc(void **state) +{ + UNUSED(state); + + char itf[] = "lo"; + uint32_t routers[] = { 0x100000a, 0x200000a }; + + struct rpc_trn_vpc_t vpc1 = { + .interface = itf, + .tunid = 3, + .routers_ips = { .routers_ips_len = 2, + .routers_ips_val = routers } + + }; + + int *rc; + expect_function_calls(__wrap_bpf_map_update_elem, 2); + rc = update_vpc_1_svc(&vpc1, NULL); + assert_int_equal(*rc, 0); +} + static void test_update_net_1_svc(void **state) { UNUSED(state); @@ -488,6 +527,28 @@ static void test_update_net_1_svc(void **state) assert_int_equal(*rc, 0); } +static void test_update_offload_net_1_svc(void **state) +{ + UNUSED(state); + + char itf[] = "lo"; + uint32_t switches[] = { 0x100000a, 0x200000a }; + + struct rpc_trn_network_t net1 = { + .interface = itf, + .prefixlen = 16, + .tunid = 3, + .netip = 0xa, + .switches_ips = { .switches_ips_len = 2, + .switches_ips_val = switches } + }; + + int *rc; + expect_function_calls(__wrap_bpf_map_update_elem, 2); + rc = update_net_1_svc(&net1, NULL); + assert_int_equal(*rc, 0); +} + static void test_update_ep_1_svc(void **state) { UNUSED(state); @@ -516,6 +577,34 @@ static void test_update_ep_1_svc(void **state) assert_int_equal(*rc, 0); } +static void test_update_offload_ep_1_svc(void **state) +{ + UNUSED(state); + + char itf[] = "lo"; + char vitf[] = "veth0"; + char hosted_itf[] = "veth"; + uint32_t remote[] = { 0x200000a }; + char mac[6] = { 1, 2, 3, 4, 5, 6 }; + + struct rpc_trn_endpoint_t ep1 = { + .interface = itf, + .ip = 0x100000a, + .eptype = 1, + .remote_ips = { .remote_ips_len = 1, .remote_ips_val = remote }, + .hosted_interface = hosted_itf, + .veth = vitf, + .tunid = 3, + }; + + memcpy(ep1.mac, mac, sizeof(char) * 6); + + int *rc; + expect_function_calls(__wrap_bpf_map_update_elem, 3); + rc = update_ep_1_svc(&ep1, NULL); + assert_int_equal(*rc, 0); +} + #if 0 static void test_update_agent_ep_1_svc(void **state) { @@ -1487,6 +1576,32 @@ static void test_delete_vpc_1_svc(void **state) assert_int_equal(*rc, RPC_TRN_ERROR); } +static void test_delete_offload_vpc_1_svc(void **state) +{ + UNUSED(state); + char itf[] = "lo"; + struct rpc_trn_vpc_key_t vpc_key = { .interface = itf, .tunid = 3 }; + int *rc; + + /* Test delete_vpc_1 with valid vp_ckey */ + will_return(__wrap_bpf_map_delete_elem, TRUE); + will_return(__wrap_bpf_map_delete_elem, TRUE); + expect_function_calls(__wrap_bpf_map_delete_elem, 2); + rc = delete_vpc_1_svc(&vpc_key, NULL); + assert_int_equal(*rc, 0); + + /* Test delete_vpc_1 with invalid vpc_key */ + will_return(__wrap_bpf_map_delete_elem, FALSE); + expect_function_call(__wrap_bpf_map_delete_elem); + rc = delete_vpc_1_svc(&vpc_key, NULL); + assert_int_equal(*rc, RPC_TRN_FATAL); + + /* Test delete_vpc_1 with invalid interface*/ + vpc_key.interface = ""; + rc = delete_vpc_1_svc(&vpc_key, NULL); + assert_int_equal(*rc, RPC_TRN_ERROR); +} + static void test_delete_net_1_svc(void **state) { UNUSED(state); @@ -1517,6 +1632,38 @@ static void test_delete_net_1_svc(void **state) assert_int_equal(*rc, RPC_TRN_ERROR); } +static void test_delete_offload_net_1_svc(void **state) +{ + UNUSED(state); + char itf[] = "lo"; + struct rpc_trn_network_key_t net_key = { + .interface = itf, + .prefixlen = 16, + .tunid = 3, + .netip = 0xa, + }; + int *rc; + + /* Test delete_net_1 with valid net_key */ + will_return(__wrap_bpf_map_delete_elem, TRUE); + will_return(__wrap_bpf_map_delete_elem, TRUE); + expect_function_calls(__wrap_bpf_map_delete_elem, 2); + rc = delete_net_1_svc(&net_key, NULL); + assert_int_equal(*rc, 0); + + /* Test delete_net_1 with invalid net_key */ + will_return(__wrap_bpf_map_delete_elem, FALSE); + expect_function_call(__wrap_bpf_map_delete_elem); + rc = delete_net_1_svc(&net_key, NULL); + assert_int_equal(*rc, RPC_TRN_ERROR); + + /* Test delete_net_1 with invalid interface*/ + net_key.interface = ""; + rc = delete_net_1_svc(&net_key, NULL); + assert_int_equal(*rc, RPC_TRN_ERROR); +} + + static void test_delete_ep_1_svc(void **state) { UNUSED(state); @@ -1566,6 +1713,56 @@ static void test_delete_ep_1_svc(void **state) assert_int_equal(*rc, RPC_TRN_ERROR); } +static void test_delete_offload_ep_1_svc(void **state) +{ + UNUSED(state); + char itf[] = "lo"; + struct rpc_trn_endpoint_key_t ep_key = { + .interface = itf, + .ip = 0x100000a, + .tunid = 3, + }; + int *rc; + + uint32_t remote[] = { 0x200000a }; + char mac[6] = { 1, 2, 3, 4, 5, 6 }; + + struct endpoint_t ep_val; + ep_val.eptype = 1; + ep_val.nremote_ips = 1; + ep_val.remote_ips[0] = remote[0]; + ep_val.hosted_iface = 1; + memcpy(ep_val.mac, mac, sizeof(mac)); + + /* Test delete_ep_1 with valid ep_key */ + will_return(__wrap_bpf_map_lookup_elem, &ep_val); + will_return(__wrap_bpf_map_lookup_elem, NULL); + will_return(__wrap_bpf_map_lookup_elem, NULL); + will_return(__wrap_bpf_map_lookup_elem, NULL); + will_return(__wrap_bpf_map_delete_elem, TRUE); + will_return(__wrap_bpf_map_delete_elem, TRUE); + expect_function_call(__wrap_bpf_map_lookup_elem); + expect_function_calls(__wrap_bpf_map_delete_elem, 2); + rc = delete_ep_1_svc(&ep_key, NULL); + assert_int_equal(*rc, 0); + + /* Test delete_ep_1 with invalid ep_key */ + will_return(__wrap_bpf_map_lookup_elem, &ep_val); + will_return(__wrap_bpf_map_lookup_elem, NULL); + will_return(__wrap_bpf_map_lookup_elem, NULL); + will_return(__wrap_bpf_map_lookup_elem, NULL); + will_return(__wrap_bpf_map_delete_elem, FALSE); + expect_function_call(__wrap_bpf_map_lookup_elem); + expect_function_call(__wrap_bpf_map_delete_elem); + rc = delete_ep_1_svc(&ep_key, NULL); + assert_int_equal(*rc, RPC_TRN_ERROR); + + /* Test delete_ep_1 with invalid interface*/ + ep_key.interface = ""; + rc = delete_ep_1_svc(&ep_key, NULL); + assert_int_equal(*rc, RPC_TRN_ERROR); +} + static void test_delete_agent_ep_1_svc(void **state) { UNUSED(state); @@ -1656,6 +1853,16 @@ static int groupSetup(void **state) return 0; } +static int offload_groupSetup(void **state) +{ + UNUSED(state); + TRN_LOG_INIT("transitd_offload_unit"); + trn_itf_table_init(); + do_lo_xdp_load(); + do_lo_offload_xdp_load(); + return 0; +} + /** * This is run once after all group tests */ @@ -1709,7 +1916,18 @@ int main() cmocka_unit_test(test_delete_agent_network_policy_protocol_port_1_svc) }; + const struct CMUnitTest offload_tests[] = { + cmocka_unit_test(test_update_offload_vpc_1_svc), + cmocka_unit_test(test_update_offload_net_1_svc), + cmocka_unit_test(test_update_offload_ep_1_svc), + cmocka_unit_test(test_delete_offload_vpc_1_svc), + cmocka_unit_test(test_delete_offload_net_1_svc), + cmocka_unit_test(test_delete_offload_ep_1_svc) + }; + int result = cmocka_run_group_tests(tests, groupSetup, groupTeardown); + result = cmocka_run_group_tests(offload_tests, offload_groupSetup, groupTeardown); + return result; } diff --git a/src/dmn/trn_rpc_protocol_handlers_1.c b/src/dmn/trn_rpc_protocol_handlers_1.c index 085ff9b9..308abf29 100644 --- a/src/dmn/trn_rpc_protocol_handlers_1.c +++ b/src/dmn/trn_rpc_protocol_handlers_1.c @@ -616,53 +616,73 @@ int *load_transit_xdp_1_svc(rpc_trn_xdp_intf_t *xdp_intf, struct svc_req *rqstp) struct user_metadata_t empty_md; struct user_metadata_t *md = trn_itf_table_find(itf); - if (md) { - TRN_LOG_INFO("Transit XDP for interface %s already exist.", itf); - return &result; - } + if (xdp_flag == XDP_FLAGS_HW_MODE) { + if (!md) { + TRN_LOG_ERROR("Cannot find interface metadata for %s", itf); + result = RPC_TRN_FATAL; + return &result; + } - TRN_LOG_INFO("Loading transit XDP for interface %s.", itf); - md = malloc(sizeof(struct user_metadata_t)); - if (!md) { - TRN_LOG_ERROR("Failure allocating memory for user_metadata_t"); - result = RPC_TRN_FATAL; - goto error; - } + // Metadata has been initialized in XDP_FLAGS_SKB_MODE + rc = trn_user_metadata_init_offload(md, itf, kern_path, xdp_flag); + if (rc != 0) { + TRN_LOG_ERROR( + "Failure initializing or loading transit XDP offload program for interface %s", + itf); + result = RPC_TRN_FATAL; + return &result; + } - memset(md, 0, sizeof(struct user_metadata_t)); - // Set all interface index slots to unused - int i; - for (i = 0; i < TRAN_MAX_ITF; i++) { - md->itf_idx[i] = TRAN_UNUSED_ITF_IDX; - } + TRN_LOG_INFO("Successfully loaded transit XDP offload on interface %s", itf); + } else { + if (md) { + TRN_LOG_INFO("Transit XDP for interface %s already exist.", itf); + return &result; + } - strcpy(md->pcapfile, xdp_intf->pcapfile); - md->pcapfile[255] = '\0'; - md->xdp_flags = xdp_intf->xdp_flag; + TRN_LOG_INFO("Loading transit XDP for interface %s.", itf); + md = malloc(sizeof(struct user_metadata_t)); + if (!md) { + TRN_LOG_ERROR("Failure allocating memory for user_metadata_t"); + result = RPC_TRN_FATAL; + goto error; + } - TRN_LOG_DEBUG("load_transit_xdp_1 path: %s, pcap: %s", - xdp_intf->xdp_path, xdp_intf->pcapfile); + memset(md, 0, sizeof(struct user_metadata_t)); + // Set all interface index slots to unused + int i; + for (i = 0; i < TRAN_MAX_ITF; i++) { + md->itf_idx[i] = TRAN_UNUSED_ITF_IDX; + } - rc = trn_user_metadata_init(md, itf, kern_path, md->xdp_flags); - if (rc != 0) { - TRN_LOG_ERROR( - "Failure initializing or loading transit XDP program for interface %s", - itf); - result = RPC_TRN_FATAL; - goto error; - } + strcpy(md->pcapfile, xdp_intf->pcapfile); + md->pcapfile[255] = '\0'; + md->xdp_flags = xdp_intf->xdp_flag; - rc = trn_itf_table_insert(itf, md); - if (rc != 0) { - TRN_LOG_ERROR( - "Failure populating interface table when loading XDP program on %s", - itf); - result = RPC_TRN_ERROR; - unload_error = true; - goto error; - } + TRN_LOG_DEBUG("load_transit_xdp_1 path: %s, pcap: %s", + xdp_intf->xdp_path, xdp_intf->pcapfile); - TRN_LOG_INFO("Successfully loaded transit XDP on interface %s", itf); + rc = trn_user_metadata_init(md, itf, kern_path, md->xdp_flags); + if (rc != 0) { + TRN_LOG_ERROR( + "Failure initializing or loading transit XDP program for interface %s", + itf); + result = RPC_TRN_FATAL; + goto error; + } + + rc = trn_itf_table_insert(itf, md); + if (rc != 0) { + TRN_LOG_ERROR( + "Failure populating interface table when loading XDP program on %s", + itf); + result = RPC_TRN_ERROR; + unload_error = true; + goto error; + } + + TRN_LOG_INFO("Successfully loaded transit XDP on interface %s", itf); + } return &result; diff --git a/src/dmn/trn_transit_xdp_usr.c b/src/dmn/trn_transit_xdp_usr.c index e69cfa78..7dc9b1e7 100644 --- a/src/dmn/trn_transit_xdp_usr.c +++ b/src/dmn/trn_transit_xdp_usr.c @@ -220,6 +220,25 @@ int trn_update_network(struct user_metadata_t *md, struct network_key_t *netkey, TRN_LOG_ERROR("Store network mapping failed (err:%d)", err); return 1; } + + if (md->xdp_flags == XDP_FLAGS_HW_MODE) { + struct network_offload_t net_offload; + if (net->nswitches > TRAN_MAX_NSWITCH_OFFLOAD) { + TRN_LOG_ERROR("Store offloaded network mapping failed for exceeding TRAN_MAX_NSWITCH_OFFLOAD"); + return 1; + } + + net_offload.prefixlen = net->prefixlen; + memcpy(net_offload.nip, net->nip, sizeof(net_offload.nip)); + net_offload.nswitches = net->nswitches; + memcpy(net_offload.switches_ips, net->switches_ips, net->nswitches * sizeof(net_offload.switches_ips[0])); + err = bpf_map_update_elem(md->networks_offload_map_fd, netkey, &net_offload, 0); + if (err) { + TRN_LOG_ERROR("Store offloaded network mapping failed (err:%d)", err); + return 1; + } + } + return 0; } @@ -280,6 +299,25 @@ int trn_update_endpoint(struct user_metadata_t *md, return 1; } + if (md->xdp_flags == XDP_FLAGS_HW_MODE) { + struct endpoint_offload_t ep_offload; + if (ep->nremote_ips > TRAN_MAX_REMOTES_OFFLOAD) { + TRN_LOG_ERROR("Store offloaded endpoint mapping failed for exceeding TRAN_MAX_REMOTES_OFFLOAD"); + return 1; + } + + ep_offload.eptype = ep->eptype; + ep_offload.nremote_ips = ep->nremote_ips; + memcpy(ep_offload.remote_ips, ep->remote_ips, ep->nremote_ips * sizeof(ep_offload.remote_ips[0])); + ep_offload.hosted_iface = ep->hosted_iface; + memcpy(ep_offload.mac, ep->mac, sizeof(ep->mac)); + err = bpf_map_update_elem(md->endpoints_offload_map_fd, epkey, &ep_offload, 0); + if (err) { + TRN_LOG_ERROR("Store offloaded endpoint mapping failed (err:%d).", err); + return 1; + } + } + return 0; } @@ -291,6 +329,23 @@ int trn_update_vpc(struct user_metadata_t *md, struct vpc_key_t *vpckey, TRN_LOG_ERROR("Store VPCs mapping failed (err:%d).", err); return 1; } + + if (md->xdp_flags == XDP_FLAGS_HW_MODE) { + struct vpc_offload_t vpc_offload; + if (vpc->nrouters > TRAN_MAX_NROUTER_OFFLOAD) { + TRN_LOG_ERROR("Store offloaded vpc mapping failed for exceeding TRAN_MAX_NROUTER_OFFLOAD"); + return 1; + } + + vpc_offload.nrouters = vpc->nrouters; + memcpy(vpc_offload.routers_ips, vpc->routers_ips, vpc->nrouters * sizeof(vpc_offload.routers_ips[0])); + err = bpf_map_update_elem(md->vpc_offload_map_fd, vpckey, &vpc_offload, 0); + if (err) { + TRN_LOG_ERROR("Store offloaded vpc mapping failed (err:%d).", err); + return 1; + } + } + return 0; } @@ -491,6 +546,15 @@ int trn_delete_network(struct user_metadata_t *md, struct network_key_t *netkey) TRN_LOG_ERROR("Deleting network mapping failed (err:%d).", err); return 1; } + + if (md->xdp_flags == XDP_FLAGS_HW_MODE) { + err = bpf_map_delete_elem(md->networks_offload_map_fd, netkey); + if (err) { + TRN_LOG_ERROR("Deleting offload network mapping failed (err:%d).", err); + return 1; + } + } + return 0; } @@ -518,6 +582,15 @@ int trn_delete_endpoint(struct user_metadata_t *md, return 1; } + if (md->xdp_flags == XDP_FLAGS_HW_MODE) { + err = bpf_map_delete_elem(md->endpoints_offload_map_fd, epkey); + if (err) { + TRN_LOG_ERROR("Deleting offload endpoint mapping failed (err:%d).", + err); + return 1; + } + } + return 0; } @@ -528,6 +601,15 @@ int trn_delete_vpc(struct user_metadata_t *md, struct vpc_key_t *vpckey) TRN_LOG_ERROR("Deleting vpc mapping failed (err:%d).", err); return 1; } + + if (md->xdp_flags == XDP_FLAGS_HW_MODE) { + err = bpf_map_delete_elem(md->vpc_offload_map_fd, vpckey); + if (err) { + TRN_LOG_ERROR("Deleting offload vpc mapping failed (err:%d).", err); + return 1; + } + } + return 0; } @@ -645,6 +727,82 @@ int trn_user_metadata_init(struct user_metadata_t *md, char *itf, return 0; } +int trn_user_metadata_init_offload(struct user_metadata_t *md, char *itf, + char *kern_path, int xdp_flags) +{ + int rc; + struct rlimit r = { RLIM_INFINITY, RLIM_INFINITY }; + struct bpf_prog_load_attr prog_load_attr = { .prog_type = + BPF_PROG_TYPE_XDP, + .file = kern_path }; + __u32 info_len = sizeof(md->info_offload); + + if (setrlimit(RLIMIT_MEMLOCK, &r)) { + TRN_LOG_ERROR("setrlimit(RLIMIT_MEMLOCK)"); + return 1; + } + + md->ifindex = if_nametoindex(itf); + prog_load_attr.ifindex = md->ifindex; + if (!md->ifindex) { + TRN_LOG_ERROR("if_nametoindex"); + return 1; + } + + md->eth.ip = trn_get_interface_ipv4(md->ifindex); + md->eth.iface_index = md->ifindex; + + // offload_xdp cannot reuse the pinned maps(network policy) + if (bpf_prog_load_xattr(&prog_load_attr, &md->obj_offload, &md->prog_offload_fd)) { + TRN_LOG_ERROR("Error loading bpf: %s", kern_path); + return 1; + } + + // map_init + md->networks_offload_map = bpf_map__next(NULL, md->obj_offload); + md->vpc_offload_map = bpf_map__next(md->networks_offload_map, md->obj_offload); + md->endpoints_offload_map = bpf_map__next(md->vpc_offload_map, md->obj_offload); + md->interface_config_offload_map = bpf_map__next(md->endpoints_offload_map, md->obj_offload); + if (!md->endpoints_offload_map || !md->interface_config_offload_map || + !md->networks_offload_map || !md->vpc_offload_map) { + TRN_LOG_ERROR("Failure finding offloaded maps objects."); + return 1; + } + md->networks_offload_map_fd = bpf_map__fd(md->networks_offload_map); + md->vpc_offload_map_fd = bpf_map__fd(md->vpc_offload_map); + md->endpoints_offload_map_fd = bpf_map__fd(md->endpoints_offload_map); + md->interface_config_offload_map_fd = bpf_map__fd(md->interface_config_offload_map); + // map_init done + + if (!md->prog_offload_fd) { + TRN_LOG_ERROR("load_bpf_file: %s.", strerror(errno)); + return 1; + } + + if (bpf_set_link_xdp_fd(md->ifindex, md->prog_offload_fd, xdp_flags) < 0) { + TRN_LOG_ERROR("link set xdp_offload fd failed - %s.", strerror(errno)); + return 1; + } + + rc = bpf_obj_get_info_by_fd(md->prog_offload_fd, &md->info_offload, &info_len); + if (rc != 0) { + TRN_LOG_ERROR("can't get prog info - %s.", strerror(errno)); + return rc; + } + md->prog_offload_id = md->info_offload.id; + + // As the config of original Transit Program already has the itf_idx, set Offload Program as the same config + int k = 0; + rc = bpf_map_update_elem(md->interface_config_offload_map_fd, &k, &md->eth, 0); + if (rc != 0) { + TRN_LOG_ERROR("Failed to store interface data."); + return 1; + } + + md->xdp_flags = xdp_flags; // overwrite xdp_flags with XDP_OFFLOAD + return 0; +} + uint32_t trn_get_interface_ipv4(int itf_idx) { int fd; diff --git a/src/dmn/trn_transit_xdp_usr.h b/src/dmn/trn_transit_xdp_usr.h index c17ff797..e71a2223 100644 --- a/src/dmn/trn_transit_xdp_usr.h +++ b/src/dmn/trn_transit_xdp_usr.h @@ -117,6 +117,8 @@ struct user_metadata_t { __u32 xdp_flags; int prog_fd; __u32 prog_id; + int prog_offload_fd; + __u32 prog_offload_id; char pcapfile[256]; int itf_idx[TRAN_MAX_ITF]; @@ -148,6 +150,10 @@ struct user_metadata_t { int ing_namespace_label_policy_map_fd; int ing_pod_and_namespace_label_policy_map_fd; int tx_stats_map_fd; + int networks_offload_map_fd; + int vpc_offload_map_fd; + int endpoints_offload_map_fd; + int interface_config_offload_map_fd; struct bpf_map *jmp_table_map; struct bpf_map *networks_map; @@ -177,9 +183,15 @@ struct user_metadata_t { struct bpf_map *ing_namespace_label_policy_map; struct bpf_map *ing_pod_and_namespace_label_policy_map; struct bpf_map *tx_stats_map; + struct bpf_map *networks_offload_map; + struct bpf_map *vpc_offload_map; + struct bpf_map *endpoints_offload_map; + struct bpf_map *interface_config_offload_map; struct bpf_prog_info info; struct bpf_object *obj; + struct bpf_prog_info info_offload; + struct bpf_object *obj_offload; /* Array of programs at different stages. Currently supporting only one extra tail-call */ struct ebpf_prog_stage_t ebpf_progs[TRAN_MAX_PROG]; @@ -221,6 +233,9 @@ int trn_delete_network(struct user_metadata_t *md, int trn_user_metadata_init(struct user_metadata_t *md, char *itf, char *kern_path, int xdp_flags); +int trn_user_metadata_init_offload(struct user_metadata_t *md, char *itf, + char *kern_path, int xdp_flags); + uint32_t trn_get_interface_ipv4(int itf_idx); int trn_add_prog(struct user_metadata_t *md, unsigned int prog_idx, diff --git a/src/include/trn_datamodel.h b/src/include/trn_datamodel.h index c20f1fd6..99f42199 100644 --- a/src/include/trn_datamodel.h +++ b/src/include/trn_datamodel.h @@ -32,6 +32,9 @@ #define TRAN_MAX_NSWITCH 128 #define TRAN_MAX_NROUTER 128 #define TRAN_MAX_REMOTES 128 +#define TRAN_MAX_NSWITCH_OFFLOAD 5 +#define TRAN_MAX_NROUTER_OFFLOAD 5 +#define TRAN_MAX_REMOTES_OFFLOAD 7 #define TRAN_MAX_ITF 128 #define TRAN_UNUSED_ITF_IDX -1 @@ -95,6 +98,14 @@ struct endpoint_t { unsigned char mac[6]; } __attribute__((packed, aligned(4))); +struct endpoint_offload_t { + __u32 eptype; + __u32 nremote_ips; + __u32 remote_ips[TRAN_MAX_REMOTES_OFFLOAD]; //cause the size of remote_ips[TRAN_MAX_REMOTES] is too big to offload + int hosted_iface; + unsigned char mac[6]; +} __attribute__((packed, aligned(4))); + struct packet_metadata_key_t { __u32 tunip[3]; } __attribute__((packed)); @@ -126,6 +137,13 @@ struct network_t { __u32 switches_ips[TRAN_MAX_NSWITCH]; } __attribute__((packed, aligned(4))); +struct network_offload_t { + __u32 prefixlen; /* up to 32 for AF_INET, 128 for AF_INET6 */ + __u32 nip[3]; + __u32 nswitches; + __u32 switches_ips[TRAN_MAX_NSWITCH_OFFLOAD]; +} __attribute__((packed, aligned(4))); + struct vpc_key_t { union { __be64 tunnel_id; @@ -137,6 +155,11 @@ struct vpc_t { __u32 routers_ips[TRAN_MAX_NROUTER]; } __attribute__((packed, aligned(4))); +struct vpc_offload_t { + __u32 nrouters; + __u32 routers_ips[TRAN_MAX_NROUTER_OFFLOAD]; +} __attribute__((packed, aligned(4))); + struct tunnel_iface_t { int iface_index; __u32 ip; diff --git a/src/xdp/trn_transit_xdp_hardware_offload.c b/src/xdp/trn_transit_xdp_hardware_offload.c new file mode 100644 index 00000000..8ac560ee --- /dev/null +++ b/src/xdp/trn_transit_xdp_hardware_offload.c @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file trn_transit_xdp_hardware_offload.c + * @author Peng Yang (@yangpenger) + * + * @brief Offloads functions of bouncers and dividers about Direct Path. + * This offloaded program works before original Transit XDP program, + * i.e., multiple programs on the same XDP interface. + * Thus, non-offload functions are performed by original Transit XDP program. + * + * @copyright Copyright (c) 2019 The Authors. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "extern/bpf_endian.h" +#include "extern/bpf_helpers.h" +#include "trn_datamodel.h" +#include "trn_kern.h" + +int _version SEC("version") = 1; + +struct bpf_map_def SEC("maps") networks_offload_map = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(struct network_key_t), + .value_size = sizeof(struct network_offload_t), + .max_entries = 1000001, + .map_flags = 0, +}; +BPF_ANNOTATE_KV_PAIR(networks_offload_map, struct network_key_t, struct network_offload_t); + +struct bpf_map_def SEC("maps") vpc_offload_map = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(struct vpc_key_t), + .value_size = sizeof(struct vpc_offload_t), + .max_entries = 1000001, + .map_flags = 0, +}; +BPF_ANNOTATE_KV_PAIR(vpc_offload_map, struct vpc_key_t, struct vpc_offload_t); + +struct bpf_map_def SEC("maps") endpoints_offload_map = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(struct endpoint_key_t), + .value_size = sizeof(struct endpoint_offload_t), + .max_entries = 1000001, + .map_flags = 0, +}; +BPF_ANNOTATE_KV_PAIR(endpoints_offload_map, struct endpoint_key_t, struct endpoint_offload_t); + +struct bpf_map_def SEC("maps") interface_config_offload_map = { + .type = BPF_MAP_TYPE_ARRAY, + .key_size = sizeof(int), + .value_size = sizeof(struct tunnel_iface_t), + .max_entries = 1, + .map_flags = 0, +}; +BPF_ANNOTATE_KV_PAIR(interface_config_offload_map, int, struct tunnel_iface_t); + +static __inline int trn_rewrite_remote_mac(struct transit_packet *pkt) +{ + /* The TTL must have been decremented before this step, Drop the + packet if TTL is zero */ + if (!pkt->ip->ttl) + return XDP_DROP; + + struct endpoint_offload_t *remote_ep; + struct endpoint_key_t epkey; + epkey.tunip[0] = 0; + epkey.tunip[1] = 0; + epkey.tunip[2] = pkt->ip->daddr; + /* Get the remote_mac address based on the value of the outer dest IP */ + remote_ep = bpf_map_lookup_elem(&endpoints_offload_map, &epkey); + if (!remote_ep) { + return XDP_DROP; + } + + trn_set_src_mac(pkt->data, pkt->eth->h_dest); + trn_set_dst_mac(pkt->data, remote_ep->mac); + + if (pkt->ip->tos & IPTOS_MINCOST) { + return XDP_PASS; + } + + return XDP_TX; +} + +static __inline int trn_router_handle_pkt(struct transit_packet *pkt, + __u32 inner_src_ip, + __u32 inner_dst_ip) +{ + __be64 tunnel_id = trn_vni_to_tunnel_id(pkt->geneve->vni); + /* This is where we forward the packet to the transit router: First lookup + the network of the inner_ip->daddr, if found hash and forward to the + transit switch of that network, OW forward to the transit router. */ + + struct network_key_t nkey; + struct network_offload_t *net; + /* SmartNIC does not supporting BPF_MAP_TYPE_LPM_TRIE here, so match with a exact length (80). */ + nkey.prefixlen = 80; + __builtin_memcpy(&nkey.nip[0], &tunnel_id, sizeof(tunnel_id)); + /* Obtain the network number by the mask. The subnet prefix length is 16. */ + nkey.nip[2] = inner_dst_ip & 0xFFFF; + net = bpf_map_lookup_elem(&networks_offload_map, &nkey); + + if (net) { + pkt->rts_opt->rts_data.host.ip = pkt->ip->daddr; + __builtin_memcpy(pkt->rts_opt->rts_data.host.mac, + pkt->eth->h_dest, 6 * sizeof(unsigned char)); + + if (net->nip[0] != nkey.nip[0] || net->nip[1] != nkey.nip[1]) { + return XDP_DROP; + } + + /* Only send to the first switch. */ + __u32 swidx = 0; + trn_set_src_dst_ip_csum(pkt, pkt->ip->daddr, + net->switches_ips[swidx]); + + return trn_rewrite_remote_mac(pkt); + } + + /* Now forward the packet to the VPC router */ + struct vpc_key_t vpckey; + struct vpc_offload_t *vpc; + + vpckey.tunnel_id = tunnel_id; + vpc = bpf_map_lookup_elem(&vpc_offload_map, &vpckey); + if (!vpc) { + return XDP_DROP; + } + + /* Only send to the first router. */ + __u32 routeridx = 0; + trn_set_src_dst_ip_csum(pkt, pkt->ip->daddr, + vpc->routers_ips[routeridx]); + + return trn_rewrite_remote_mac(pkt); +} + + +static __inline int trn_switch_handle_pkt(struct transit_packet *pkt, + __u32 inner_src_ip, + __u32 inner_dst_ip, __u32 orig_src_ip) +{ + __be64 tunnel_id = trn_vni_to_tunnel_id(pkt->geneve->vni); + struct endpoint_offload_t *ep; + struct endpoint_key_t epkey; + + __builtin_memcpy(&epkey.tunip[0], &tunnel_id, sizeof(tunnel_id)); + epkey.tunip[2] = inner_dst_ip; + + /* Get the remote_ip based on the value of the inner dest IP and VNI*/ + ep = bpf_map_lookup_elem(&endpoints_offload_map, &epkey); + if (!ep) { + if (pkt->scaled_ep_opt->type == TRN_GNV_SCALED_EP_OPT_TYPE && + pkt->scaled_ep_opt->scaled_ep_data.msg_type == + TRN_SCALED_EP_MODIFY) + return XDP_PASS; + + return trn_router_handle_pkt(pkt, inner_src_ip, inner_dst_ip); + } + + /* The packet may be sent first to a gw mac address */ + trn_set_dst_mac(pkt->inner_eth, ep->mac); + + // TODO: Currently all endpoints are attached to one host, for some + // ep types, they will have multiple attachments (e.g. LB endpoint). + if (ep->hosted_iface != -1) { + return XDP_PASS; + } + + if (ep->eptype == TRAN_SCALED_EP) { + return XDP_PASS; + } + + if (ep->nremote_ips == 0) { + return XDP_DROP; + } + + trn_set_src_dst_ip_csum(pkt, pkt->ip->daddr, ep->remote_ips[0]); + + return trn_rewrite_remote_mac(pkt); +} + +static __inline int trn_process_inner_ip(struct transit_packet *pkt) +{ + pkt->inner_ip = (void *)pkt->inner_eth + pkt->inner_eth_off; + __u32 ipproto; + + if (pkt->inner_ip + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + /* For whatever compiler reason, we need to perform reverse flow modification + in this function instead of trn_switch_handle_pkt so we keep the orig_src_ip */ + __u32 orig_src_ip = pkt->inner_ip->saddr; + + pkt->inner_ipv4_tuple.saddr = pkt->inner_ip->saddr; + pkt->inner_ipv4_tuple.daddr = pkt->inner_ip->daddr; + pkt->inner_ipv4_tuple.protocol = pkt->inner_ip->protocol; + pkt->inner_ipv4_tuple.sport = 0; + pkt->inner_ipv4_tuple.dport = 0; + + if (pkt->inner_ipv4_tuple.protocol == IPPROTO_TCP) { + pkt->inner_tcp = (void *)pkt->inner_ip + sizeof(*pkt->inner_ip); + if (pkt->inner_tcp + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + pkt->inner_ipv4_tuple.sport = pkt->inner_tcp->source; + pkt->inner_ipv4_tuple.dport = pkt->inner_tcp->dest; + } + + if (pkt->inner_ipv4_tuple.protocol == IPPROTO_UDP) { + pkt->inner_udp = (void *)pkt->inner_ip + sizeof(*pkt->inner_ip); + if (pkt->inner_udp + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + pkt->inner_ipv4_tuple.sport = pkt->inner_udp->source; + pkt->inner_ipv4_tuple.dport = pkt->inner_udp->dest; + } + + __be64 tunnel_id = trn_vni_to_tunnel_id(pkt->geneve->vni); + + /* Lookup the source endpoint*/ + struct endpoint_offload_t *src_ep; + struct endpoint_key_t src_epkey; + __builtin_memcpy(&src_epkey.tunip[0], &tunnel_id, sizeof(tunnel_id)); + src_epkey.tunip[2] = pkt->inner_ip->saddr; + src_ep = bpf_map_lookup_elem(&endpoints_offload_map, &src_epkey); + + /* If this is not the source endpoint's host, + skip reverse flow modification, or scaled endpoint modify handling */ + if (pkt->scaled_ep_opt->type == TRN_GNV_SCALED_EP_OPT_TYPE && + pkt->scaled_ep_opt->scaled_ep_data.msg_type == + TRN_SCALED_EP_MODIFY && + src_ep && src_ep->hosted_iface != -1) { + return XDP_PASS; + } + + /* Check if we need to apply a reverse flow update */ + struct ipv4_tuple_t inner; + __builtin_memcpy(&inner, &pkt->inner_ipv4_tuple, + sizeof(struct ipv4_tuple_t)); + + return trn_switch_handle_pkt(pkt, pkt->inner_ip->saddr, + pkt->inner_ip->daddr, orig_src_ip); +} + +static __inline int trn_process_inner_arp(struct transit_packet *pkt) +{ + unsigned char *sha; + unsigned char *tha = NULL; + struct endpoint_offload_t *ep; + struct endpoint_key_t epkey; + struct endpoint_offload_t *remote_ep; + __u32 *sip, *tip; + __u64 csum = 0; + + pkt->inner_arp = (void *)pkt->inner_eth + sizeof(*pkt->inner_eth); + if (pkt->inner_arp + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + if (pkt->inner_arp->ar_pro != bpf_htons(ETH_P_IP) || + pkt->inner_arp->ar_hrd != bpf_htons(ARPHRD_ETHER)) { + return XDP_DROP; + } + + if (pkt->inner_arp->ar_op != bpf_htons(ARPOP_REPLY) && + pkt->inner_arp->ar_op != bpf_htons(ARPOP_REQUEST)) { + return XDP_DROP; + } + + if ((unsigned char *)(pkt->inner_arp + 1) > pkt->data_end) { + return XDP_ABORTED; + } + + sha = (unsigned char *)(pkt->inner_arp + 1); + if (sha + ETH_ALEN > pkt->data_end) { + return XDP_ABORTED; + } + + sip = (__u32 *)(sha + ETH_ALEN); + if (sip + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + tha = (unsigned char *)sip + sizeof(__u32); + if (tha + ETH_ALEN > pkt->data_end) { + return XDP_ABORTED; + } + + tip = (__u32 *)(tha + ETH_ALEN); + if ((void *)tip + sizeof(__u32) > pkt->data_end) { + return XDP_ABORTED; + } + + __be64 tunnel_id = trn_vni_to_tunnel_id(pkt->geneve->vni); + + __builtin_memcpy(&epkey.tunip[0], &tunnel_id, sizeof(tunnel_id)); + epkey.tunip[2] = *tip; + ep = bpf_map_lookup_elem(&endpoints_offload_map, &epkey); + /* Don't respond to arp if endpoint is not found, or it is local to host */ + if (!ep || ep->hosted_iface != -1 || + pkt->inner_arp->ar_op != bpf_htons(ARPOP_REQUEST)) { + return trn_switch_handle_pkt(pkt, *sip, *tip, *sip); + } + + /* Respond to ARP */ + pkt->inner_arp->ar_op = bpf_htons(ARPOP_REPLY); + trn_set_arp_ha(tha, sha); + trn_set_arp_ha(sha, ep->mac); + + __u32 tmp_ip = *sip; + *sip = *tip; + *tip = tmp_ip; + + /* Set the sender mac address to the ep mac address */ + trn_set_src_mac(pkt->inner_eth, ep->mac); + + if (ep->eptype == TRAN_SIMPLE_EP) { + /*Get the remote_ep address based on the value of the outer dest IP */ + epkey.tunip[0] = 0; + epkey.tunip[1] = 0; + epkey.tunip[2] = ep->remote_ips[0]; + remote_ep = bpf_map_lookup_elem(&endpoints_offload_map, &epkey); + if (!remote_ep) { + return XDP_DROP; + } + + /* For a simple endpoint, Write the RTS option on behalf of the target endpoint */ + pkt->rts_opt->rts_data.host.ip = ep->remote_ips[0]; + __builtin_memcpy(pkt->rts_opt->rts_data.host.mac, + remote_ep->mac, 6 * sizeof(unsigned char)); + } else { + trn_reset_rts_opt(pkt); + } + + /* We need to lookup the endpoint again, since tip has changed */ + epkey.tunip[2] = *tip; + ep = bpf_map_lookup_elem(&endpoints_offload_map, &epkey); + + return trn_switch_handle_pkt(pkt, *sip, *tip, *sip); +} + +static __inline int trn_process_inner_eth(struct transit_packet *pkt) +{ + pkt->inner_eth = (void *)pkt->geneve + pkt->gnv_hdr_len; + pkt->inner_eth_off = sizeof(*pkt->inner_eth); + if (pkt->inner_eth + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + /* ARP */ + if (pkt->inner_eth->h_proto == bpf_htons(ETH_P_ARP)) { + return trn_process_inner_arp(pkt); + } + + if (pkt->eth->h_proto != bpf_htons(ETH_P_IP)) { + return XDP_DROP; + } + + return trn_process_inner_ip(pkt); +} + +static __inline int trn_process_geneve(struct transit_packet *pkt) +{ + pkt->geneve = (void *)pkt->udp + sizeof(*pkt->udp); + if (pkt->geneve + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + if (pkt->geneve->proto_type != bpf_htons(ETH_P_TEB)) { + return XDP_PASS; + } + + pkt->gnv_opt_len = pkt->geneve->opt_len * 4; + pkt->gnv_hdr_len = sizeof(*pkt->geneve) + pkt->gnv_opt_len; + pkt->rts_opt = (void *)&pkt->geneve->options[0]; + if (pkt->rts_opt + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + if (pkt->rts_opt->opt_class != TRN_GNV_OPT_CLASS) { + return XDP_ABORTED; + } + + // TODO: process options + pkt->scaled_ep_opt = (void *)pkt->rts_opt + sizeof(*pkt->rts_opt); + if (pkt->scaled_ep_opt + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + if (pkt->scaled_ep_opt->opt_class != TRN_GNV_OPT_CLASS) { + return XDP_ABORTED; + } + + return trn_process_inner_eth(pkt); +} + +static __inline int trn_process_udp(struct transit_packet *pkt) +{ + /* Get the UDP header */ + pkt->udp = (void *)pkt->ip + sizeof(*pkt->ip); + if (pkt->udp + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + if (pkt->udp->dest != GEN_DSTPORT) { + return XDP_PASS; + } + + return trn_process_geneve(pkt); +} + +static __inline int trn_process_ip(struct transit_packet *pkt) +{ + /* Get the IP header */ + pkt->ip = (void *)pkt->eth + pkt->eth_off; + if (pkt->ip + 1 > pkt->data_end) { + return XDP_ABORTED; + } + + if (pkt->ip->protocol != IPPROTO_UDP) { + return XDP_PASS; + } + + if (!pkt->ip->ttl) { + return XDP_DROP; + } + + /* Only process packets designated to this interface! + * In functional tests - relying on docker0 - we see such packets! + */ + if (pkt->ip->daddr != pkt->itf_ipv4) { + return XDP_DROP; + } + + return trn_process_udp(pkt); +} + +static __inline int trn_process_eth(struct transit_packet *pkt) +{ + pkt->eth = pkt->data; + pkt->eth_off = sizeof(*pkt->eth); + if (pkt->data + pkt->eth_off > pkt->data_end) { + return XDP_ABORTED; + } + + if (pkt->eth->h_proto != bpf_htons(ETH_P_IP)) { + return XDP_PASS; + } + + return trn_process_ip(pkt); +} + +SEC("transit") +int _transit(struct xdp_md *ctx) +{ + struct transit_packet pkt; + pkt.data = (void *)(long)ctx->data; + pkt.data_end = (void *)(long)ctx->data_end; + pkt.xdp = ctx; + struct tunnel_iface_t *itf; + int k = 0; + itf = bpf_map_lookup_elem(&interface_config_offload_map, &k); + if (!itf) { + return XDP_ABORTED; + } + + pkt.itf_ipv4 = itf->ip; + pkt.itf_idx = itf->iface_index; + + return trn_process_eth(&pkt); +} + +char _license[] SEC("license") = "GPL"; diff --git a/supported_xdp_offload_nics.yaml b/supported_xdp_offload_nics.yaml new file mode 100644 index 00000000..f71af639 --- /dev/null +++ b/supported_xdp_offload_nics.yaml @@ -0,0 +1,5 @@ +# The list of NICs that with offload XDP support +# vender_name: +# - "model_name" +Netronome: +- "Device 4000"