| /* |
| * Copyright (c) 2024 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define LOG_LEVEL LOG_LEVEL_DBG |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(mdns_resp_test); |
| |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <string.h> |
| |
| #include <ipv6.h> |
| |
| #include <zephyr/net/dns_sd.h> |
| #include <zephyr/net/dummy.h> |
| #include <zephyr/net/ethernet.h> |
| #include <zephyr/net/mdns_responder.h> |
| #include <zephyr/net/net_if.h> |
| #include <zephyr/net/socket.h> |
| #include <zephyr/ztest.h> |
| |
| #define NULL_CHAR_SIZE 1 |
| #define EXT_RECORDS_NUM 3 |
| #define MAX_RESP_PKTS 8 |
| #define MAX_TXT_SIZE 128 |
| #define RESPONSE_TIMEOUT (K_MSEC(250)) |
| |
| struct service_info { |
| bool used; |
| char instance[DNS_SD_INSTANCE_MAX_SIZE + NULL_CHAR_SIZE]; |
| char service[DNS_SD_SERVICE_MAX_SIZE + NULL_CHAR_SIZE]; |
| char proto[DNS_SD_PROTO_SIZE + NULL_CHAR_SIZE]; |
| char domain[DNS_SD_DOMAIN_MAX_SIZE + NULL_CHAR_SIZE]; |
| char text[MAX_TXT_SIZE]; |
| uint16_t port; |
| struct dns_sd_rec *record; |
| }; |
| |
| static struct net_if *iface1; |
| |
| static struct net_if_test { |
| uint8_t idx; /* not used for anything, just a dummy value */ |
| uint8_t mac_addr[sizeof(struct net_eth_addr)]; |
| struct net_linkaddr ll_addr; |
| } net_iface1_data; |
| |
| static const uint8_t ipv6_hdr_start[] = { |
| 0x60, 0x05, 0xe7, 0x00 |
| }; |
| |
| static const uint8_t ipv6_hdr_rest[] = { |
| 0x11, 0xff, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x74, 0x88, |
| 0x9c, 0x1b, 0x44, 0x72, 0x39, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb |
| }; |
| |
| static const uint8_t dns_sd_service_enumeration_query[] = { |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, |
| 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x07, 0x5f, 0x64, 0x6e, |
| 0x73, 0x2d, 0x73, 0x64, 0x04, 0x5f, 0x75, 0x64, 0x70, 0x05, 0x6c, 0x6f, 0x63, |
| 0x61, 0x6c, 0x00, 0x00, 0x0c, 0x00, 0x01 |
| }; |
| |
| static const uint8_t service_enum_start[] = { |
| 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x09, |
| 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x07, 0x5f, 0x64, 0x6e, |
| 0x73, 0x2d, 0x73, 0x64, 0x04, 0x5f, 0x75, 0x64, 0x70, 0x05, 0x6c, 0x6f, 0x63, |
| 0x61, 0x6c, 0x00, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x11, 0x94 |
| }; |
| |
| static const uint8_t payload_bar_udp_local[] = { |
| 0x00, 0x0c, 0x04, 0x5f, 0x62, 0x61, 0x72, 0x04, 0x5f, 0x75, 0x64, 0x70, 0xc0, |
| 0x23 |
| }; |
| |
| static const uint8_t payload_custom_tcp_local[] = { |
| 0x00, 0x0f, 0x07, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x04, |
| 0x5f, 0x74, 0x63, 0x70, 0xc0, 0x23 |
| }; |
| |
| static const uint8_t payload_foo_tcp_local[] = { |
| 0x00, 0x0c, 0x04, 0x5f, 0x66, 0x6f, 0x6f, 0x04, 0x5f, 0x74, 0x63, 0x70, 0xc0, |
| 0x23 |
| }; |
| |
| static const uint8_t payload_foo_udp_local[] = { |
| 0x00, 0x0c, 0x04, 0x5f, 0x66, 0x6f, 0x6f, 0x04, 0x5f, 0x75, 0x64, 0x70, 0xc0, |
| 0x23 |
| }; |
| |
| static uint8_t mdns_server_ipv6_addr[] = { |
| 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xfb |
| }; |
| |
| static struct in6_addr ll_addr = {{{ |
| 0xfe, 0x80, 0x43, 0xb8, 0, 0, 0, 0, 0x9f, 0x74, 0x88, 0x9c, 0x1b, 0x44, 0x72, 0x39 |
| }}}; |
| |
| static struct in6_addr sender_ll_addr = {{{ |
| 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x9f, 0x74, 0x88, 0x9c, 0x1b, 0x44, 0x72, 0x39 |
| }}}; |
| |
| static bool test_started; |
| static struct k_sem wait_data; |
| static struct net_pkt *response_pkts[MAX_RESP_PKTS]; |
| static size_t responses_count; |
| static struct service_info services[EXT_RECORDS_NUM]; |
| static struct dns_sd_rec records[EXT_RECORDS_NUM]; |
| |
| static uint8_t *net_iface_get_mac(const struct device *dev) |
| { |
| struct net_if_test *data = dev->data; |
| |
| if (data->mac_addr[2] == 0x00) { |
| /* 00-00-5E-00-53-xx Documentation RFC 7042 */ |
| data->mac_addr[0] = 0x00; |
| data->mac_addr[1] = 0x00; |
| data->mac_addr[2] = 0x5E; |
| data->mac_addr[3] = 0x00; |
| data->mac_addr[4] = 0x53; |
| data->mac_addr[5] = 0x01; |
| } |
| |
| data->ll_addr.addr = data->mac_addr; |
| data->ll_addr.len = 6U; |
| |
| return data->mac_addr; |
| } |
| |
| static void net_iface_init(struct net_if *iface) |
| { |
| uint8_t *mac = net_iface_get_mac(net_if_get_device(iface)); |
| |
| net_if_set_link_addr(iface, mac, sizeof(struct net_eth_addr), |
| NET_LINK_ETHERNET); |
| } |
| |
| static int sender_iface(const struct device *dev, struct net_pkt *pkt) |
| { |
| struct net_ipv6_hdr *hdr; |
| |
| if (!pkt->buffer) { |
| return -ENODATA; |
| } |
| |
| if (test_started) { |
| hdr = NET_IPV6_HDR(pkt); |
| |
| if (net_ipv6_addr_cmp_raw(hdr->dst, mdns_server_ipv6_addr)) { |
| if (responses_count < MAX_RESP_PKTS) { |
| net_pkt_ref(pkt); |
| response_pkts[responses_count++] = pkt; |
| k_sem_give(&wait_data); |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| static struct dummy_api net_iface_api = { |
| .iface_api.init = net_iface_init, |
| .send = sender_iface, |
| }; |
| |
| #define _ETH_L2_LAYER DUMMY_L2 |
| #define _ETH_L2_CTX_TYPE NET_L2_GET_CTX_TYPE(DUMMY_L2) |
| |
| NET_DEVICE_INIT_INSTANCE(net_iface1_test, |
| "iface1", |
| iface1, |
| NULL, |
| NULL, |
| &net_iface1_data, |
| NULL, |
| CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, |
| &net_iface_api, |
| _ETH_L2_LAYER, |
| _ETH_L2_CTX_TYPE, |
| 127); |
| |
| static void *test_setup(void) |
| { |
| struct net_if_addr *ifaddr; |
| int idx; |
| |
| memset(response_pkts, 0, sizeof(response_pkts)); |
| memset(services, 0, sizeof(services)); |
| memset(records, 0, sizeof(records)); |
| |
| responses_count = 0; |
| |
| /* Cross assign records and buffers to entries for allocation */ |
| for (int i = 0; i < EXT_RECORDS_NUM; ++i) { |
| services[i].record = &records[i]; |
| |
| records[i].instance = services[i].instance; |
| records[i].service = services[i].service; |
| records[i].proto = services[i].proto; |
| records[i].domain = services[i].domain; |
| records[i].text = services[i].text; |
| records[i].port = &services[i].port; |
| } |
| |
| mdns_responder_set_ext_records(records, EXT_RECORDS_NUM); |
| |
| /* The semaphore is there to wait the data to be received. */ |
| k_sem_init(&wait_data, 0, UINT_MAX); |
| |
| iface1 = net_if_get_by_index(1); |
| |
| zassert_not_null(iface1, "Iface1 is NULL"); |
| |
| ((struct net_if_test *) net_if_get_device(iface1)->data)->idx = |
| net_if_get_by_iface(iface1); |
| |
| idx = net_if_get_by_iface(iface1); |
| zassert_equal(idx, 1, "Invalid index iface1"); |
| |
| zassert_not_null(iface1, "Interface 1"); |
| |
| ifaddr = net_if_ipv6_addr_add(iface1, &ll_addr, |
| NET_ADDR_MANUAL, 0); |
| |
| net_ipv6_nbr_add(iface1, &sender_ll_addr, net_if_get_link_addr(iface1), false, |
| NET_IPV6_NBR_STATE_STATIC); |
| |
| zassert_not_null(ifaddr, "Failed to add LL-addr"); |
| |
| /* we need to set the addresses preferred */ |
| ifaddr->addr_state = NET_ADDR_PREFERRED; |
| |
| net_if_up(iface1); |
| |
| return NULL; |
| } |
| |
| static void free_service(struct service_info *service) |
| { |
| service->used = false; |
| service->instance[0] = '\0'; |
| service->service[0] = '\0'; |
| service->proto[0] = '\0'; |
| service->domain[0] = '\0'; |
| service->port = 0; |
| |
| } |
| |
| static void free_ext_record(struct dns_sd_rec *rec) |
| { |
| for (int i = 0; i < EXT_RECORDS_NUM; ++i) { |
| if (services[i].record == rec) { |
| free_service(&services[i]); |
| return; |
| } |
| } |
| } |
| |
| static void before(void *d) |
| { |
| ARG_UNUSED(d); |
| |
| test_started = true; |
| } |
| |
| static void cleanup(void *d) |
| { |
| ARG_UNUSED(d); |
| |
| test_started = false; |
| |
| for (size_t i = 0; i < responses_count; ++i) { |
| if (response_pkts[i]) { |
| net_pkt_unref(response_pkts[i]); |
| response_pkts[i] = NULL; |
| } |
| } |
| |
| /* Clear semaphore counter */ |
| while (k_sem_take(&wait_data, K_NO_WAIT) == 0) { |
| /* NOP */ |
| } |
| |
| for (int i = 0; i < EXT_RECORDS_NUM; ++i) { |
| if (services[i].used) { |
| free_service(&services[i]); |
| } |
| } |
| } |
| |
| static void send_msg(const uint8_t *data, size_t len) |
| { |
| struct net_pkt *pkt; |
| int res; |
| |
| pkt = net_pkt_alloc_with_buffer(iface1, NET_IPV6UDPH_LEN + len, AF_UNSPEC, |
| 0, K_FOREVER); |
| zassert_not_null(pkt, "PKT is null"); |
| |
| res = net_pkt_write(pkt, ipv6_hdr_start, sizeof(ipv6_hdr_start)); |
| zassert_equal(res, 0, "pkt write for header start failed"); |
| |
| res = net_pkt_write_be16(pkt, len + NET_UDPH_LEN); |
| zassert_equal(res, 0, "pkt write for header length failed"); |
| |
| res = net_pkt_write(pkt, ipv6_hdr_rest, sizeof(ipv6_hdr_rest)); |
| zassert_equal(res, 0, "pkt write for rest of the header failed"); |
| |
| res = net_pkt_write_be16(pkt, 5353); |
| zassert_equal(res, 0, "pkt write for UDP src port failed"); |
| |
| res = net_pkt_write_be16(pkt, 5353); |
| zassert_equal(res, 0, "pkt write for UDP dst port failed"); |
| |
| res = net_pkt_write_be16(pkt, len + NET_UDPH_LEN); |
| zassert_equal(res, 0, "pkt write for UDP length failed"); |
| |
| /* to simplify testing checking of UDP checksum is disabled in prj.conf */ |
| res = net_pkt_write_be16(pkt, 0); |
| zassert_equal(res, 0, "net_pkt_write_be16() for UDP checksum failed"); |
| |
| res = net_pkt_write(pkt, data, len); |
| zassert_equal(res, 0, "net_pkt_write() for data failed"); |
| |
| res = net_recv_data(iface1, pkt); |
| zassert_equal(res, 0, "net_recv_data() failed"); |
| } |
| |
| static struct dns_sd_rec *alloc_ext_record(const char *instance, const char *service, |
| const char *proto, const char *domain, uint8_t *txt, |
| size_t txt_len, uint16_t port) |
| { |
| for (int i = 0; i < EXT_RECORDS_NUM; ++i) { |
| if (!services[i].used) { |
| services[i].used = true; |
| |
| strcpy(services[i].instance, instance); |
| strcpy(services[i].service, service); |
| strcpy(services[i].proto, proto); |
| strcpy(services[i].domain, domain); |
| |
| if (txt && txt_len) { |
| memcpy(services[i].text, txt, txt_len); |
| } |
| |
| services[i].port = htons(port); |
| services[i].record->text_size = txt_len; |
| |
| return services[i].record; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static void check_service_type_enum_resp(struct net_pkt *pkt, const uint8_t *payload, size_t len) |
| { |
| int res; |
| |
| net_pkt_cursor_init(pkt); |
| |
| net_pkt_set_overwrite(pkt, true); |
| net_pkt_skip(pkt, NET_IPV6UDPH_LEN); |
| |
| res = net_buf_data_match(pkt->cursor.buf, pkt->cursor.pos - pkt->cursor.buf->data, |
| service_enum_start, sizeof(service_enum_start)); |
| |
| zassert_equal(res, sizeof(service_enum_start), |
| "mDNS content beginning does not match"); |
| |
| net_pkt_skip(pkt, sizeof(service_enum_start)); |
| |
| res = net_pkt_get_len(pkt) - net_pkt_get_current_offset(pkt); |
| zassert_equal(res, len, "Remaining packet's length does match payload's length"); |
| |
| res = net_buf_data_match(pkt->cursor.buf, pkt->cursor.pos - pkt->cursor.buf->data, payload, |
| len); |
| zassert_equal(res, len, "Payload does not match"); |
| } |
| |
| ZTEST(test_mdns_responder, test_external_records) |
| { |
| int res; |
| struct dns_sd_rec *records[EXT_RECORDS_NUM]; |
| |
| /* mDNS responder can advertise only ports that are bound - reuse its own port */ |
| DNS_SD_REGISTER_UDP_SERVICE(foo, "zephyr", "_foo", "local", DNS_SD_EMPTY_TXT, 5353); |
| |
| records[0] = alloc_ext_record("test_rec", "_custom", "_tcp", "local", NULL, 0, 5353); |
| zassert_not_null(records[0], "Failed to alloc the record"); |
| |
| records[1] = alloc_ext_record("foo", "_bar", "_udp", "local", NULL, 0, 5353); |
| zassert_not_null(records[1], "Failed to alloc the record"); |
| |
| records[2] = alloc_ext_record("bar", "_foo", "_tcp", "local", NULL, 0, 5353); |
| zassert_not_null(records[2], "Failed to alloc the record"); |
| |
| /* Request service type enumeration */ |
| send_msg(dns_sd_service_enumeration_query, sizeof(dns_sd_service_enumeration_query)); |
| |
| /* Expect 4 packets */ |
| for (int i = 0; i < 4; ++i) { |
| res = k_sem_take(&wait_data, RESPONSE_TIMEOUT); |
| zassert_equal(res, 0, "Did not receive a response number %d", i + 1); |
| } |
| |
| /* Responder always starts with statically allocated services */ |
| check_service_type_enum_resp(response_pkts[0], payload_foo_udp_local, |
| sizeof(payload_foo_udp_local)); |
| |
| /* Responder iterates through external records backwards so check responses in LIFO seq. */ |
| check_service_type_enum_resp(response_pkts[1], payload_foo_tcp_local, |
| sizeof(payload_foo_tcp_local)); |
| check_service_type_enum_resp(response_pkts[2], payload_bar_udp_local, |
| sizeof(payload_bar_udp_local)); |
| check_service_type_enum_resp(response_pkts[3], payload_custom_tcp_local, |
| sizeof(payload_custom_tcp_local)); |
| |
| /* Remove record from the middle */ |
| free_ext_record(records[1]); |
| |
| /* Repeat service type enumeration */ |
| send_msg(dns_sd_service_enumeration_query, sizeof(dns_sd_service_enumeration_query)); |
| |
| /* Expect 3 packets */ |
| for (int i = 0; i < 3; ++i) { |
| res = k_sem_take(&wait_data, RESPONSE_TIMEOUT); |
| zassert_equal(res, 0, "Did not receive a response number %d", i + 1); |
| } |
| |
| /* Repeat checks without the removed record */ |
| check_service_type_enum_resp(response_pkts[4], payload_foo_udp_local, |
| sizeof(payload_foo_udp_local)); |
| check_service_type_enum_resp(response_pkts[5], payload_foo_tcp_local, |
| sizeof(payload_foo_tcp_local)); |
| check_service_type_enum_resp(response_pkts[6], payload_custom_tcp_local, |
| sizeof(payload_custom_tcp_local)); |
| } |
| |
| ZTEST_SUITE(test_mdns_responder, NULL, test_setup, before, cleanup, NULL); |