Skip to content

File syn_dnssd.c

File List > net > syn_dnssd.c

Go to the documentation of this file

#include "syn_dnssd.h"

#include "../port/syn_port_system.h"
#include "../util/syn_assert.h"

#include <stdio.h>
#include <string.h>

#define DNSSD_MCAST_ADDR "224.0.0.251"
#define DNSSD_TTL_PTR 4500U
#define DNSSD_TTL_SRV 120U

static void write_u16(uint8_t *buf, size_t *pos, uint16_t val)
{
    buf[(*pos)++] = (uint8_t)(val >> 8U);
    buf[(*pos)++] = (uint8_t)(val & 0xFFU);
}

static void write_u32(uint8_t *buf, size_t *pos, uint32_t val)
{
    buf[(*pos)++] = (uint8_t)(val >> 24U);
    buf[(*pos)++] = (uint8_t)(val >> 16U);
    buf[(*pos)++] = (uint8_t)(val >> 8U);
    buf[(*pos)++] = (uint8_t)(val & 0xFFU);
}

static void write_label(uint8_t *buf, size_t *pos, const char *str)
{
    size_t len = strlen(str);
    if (len > 63)
        len = 63; /* LCOV_EXCL_LINE: Defensive clamp */
    buf[(*pos)++] = (uint8_t)len;
    memcpy(&buf[*pos], str, len);
    *pos += len;
}

static void write_service_type_name(uint8_t *buf, size_t *pos, const char *service_type)
{
    /* Split service_type (e.g. "_http._tcp") */
    char temp[64];
    strncpy(temp, service_type, sizeof(temp) - 1);
    temp[sizeof(temp) - 1] = '\0';

    char *dot = strchr(temp, '.');
    if (dot != NULL) {
        *dot = '\0';
        write_label(buf, pos, temp);
        write_label(buf, pos, dot + 1);
    } else {
        write_label(buf, pos, temp);
    }
    write_label(buf, pos, "local");
    buf[(*pos)++] = 0x00; /* Null terminator */
}

static void write_instance_fqdn(uint8_t *buf, size_t *pos, const char *instance,
                                const char *service_type)
{
    write_label(buf, pos, instance);
    write_service_type_name(buf, pos, service_type);
}

static void write_hostname_fqdn(uint8_t *buf, size_t *pos, const char *hostname)
{
    write_label(buf, pos, hostname);
    write_label(buf, pos, "local");
    buf[(*pos)++] = 0x00;
}

SYN_Status syn_dnssd_init(SYN_DnsSd *sd)
{
    if (sd == NULL) {
        return SYN_ERROR;
    }
    memset(sd, 0, sizeof(*sd));

    sd->sock = syn_port_udp_open(SYN_DNSSD_PORT);
    if (sd->sock == SYN_SOCKET_INVALID) {
        return SYN_ERROR;
    }

    if (syn_port_udp_join_multicast(sd->sock, DNSSD_MCAST_ADDR) != SYN_OK) {
        syn_port_sock_close(sd->sock);
        sd->sock = SYN_SOCKET_INVALID;
        return SYN_ERROR;
    }

    return SYN_OK;
}

SYN_Status syn_dnssd_register(SYN_DnsSd *sd, const SYN_DnsSd_Service *svc)
{
    if (sd == NULL || svc == NULL || svc->instance_name == NULL || svc->service_type == NULL ||
        svc->hostname == NULL || svc->port == 0) {
        return SYN_ERROR;
    }

    if (sd->service_count >= SYN_DNSSD_MAX_SERVICES) {
        return SYN_ERROR;
    }

    sd->services[sd->service_count++] = *svc;
    return SYN_OK;
}

static bool match_qname_service(const uint8_t *buf, size_t buf_len, size_t *offset,
                                const char *service_type)
{
    /* Match query against _service._proto.local */
    size_t pos = *offset;
    char q_name[128] = {0};
    size_t q_pos = 0;

    while (pos < buf_len && buf[pos] != 0) {
        uint8_t label_len = buf[pos++];
        if (label_len > 63 || pos + label_len > buf_len) {
            return false;
        }
        if (q_pos > 0 && q_pos < sizeof(q_name) - 1) {
            q_name[q_pos++] = '.';
        }
        if (q_pos + label_len < sizeof(q_name) - 1) {
            memcpy(&q_name[q_pos], &buf[pos], label_len);
            q_pos += label_len;
        }
        pos += label_len;
    }
    if (pos < buf_len && buf[pos] == 0) {
        pos++;
    }
    q_name[q_pos] = '\0';
    *offset = pos;

    char expected[128];
    snprintf(expected, sizeof(expected), "%s.local", service_type);

    return (strcmp(q_name, expected) == 0);
}

static SYN_Status pack_service_response(const SYN_DnsSd_Service *svc, uint8_t *resp_buf,
                                        size_t max_resp_len, size_t *resp_len)
{
    if (max_resp_len < 512) {
        return SYN_ERROR;
    }

    /* Build DNS-SD Response Packet */
    size_t rpos = 0;

    /* Header: ID=0, Flags=0x8400 (Response + Authoritative), QD=0, AN=1 (PTR), NS=0, AR=3 (SRV,
     * TXT, A) */
    write_u16(resp_buf, &rpos, 0x0000);
    write_u16(resp_buf, &rpos, 0x8400);
    write_u16(resp_buf, &rpos, 0x0000); /* QDCOUNT */
    write_u16(resp_buf, &rpos, 0x0001); /* ANCOUNT (1 PTR) */
    write_u16(resp_buf, &rpos, 0x0000); /* NSCOUNT */
    write_u16(resp_buf, &rpos, 0x0003); /* ARCOUNT (SRV, TXT, A) */

    /* 1. Answer Record: PTR */
    write_service_type_name(resp_buf, &rpos, svc->service_type);
    write_u16(resp_buf, &rpos, 0x000C); /* TYPE: PTR (12) */
    write_u16(resp_buf, &rpos, 0x0001); /* CLASS: IN (1) */
    write_u32(resp_buf, &rpos, DNSSD_TTL_PTR);

    size_t ptr_rdlen_pos = rpos;
    rpos += 2; /* Reserve 2 bytes for RDLENGTH */
    size_t ptr_rdata_start = rpos;
    write_instance_fqdn(resp_buf, &rpos, svc->instance_name, svc->service_type);
    uint16_t ptr_rdlen = (uint16_t)(rpos - ptr_rdata_start);
    resp_buf[ptr_rdlen_pos] = (uint8_t)(ptr_rdlen >> 8U);
    resp_buf[ptr_rdlen_pos + 1] = (uint8_t)(ptr_rdlen & 0xFFU);

    /* 2. Additional Record 1: SRV */
    write_instance_fqdn(resp_buf, &rpos, svc->instance_name, svc->service_type);
    write_u16(resp_buf, &rpos, 0x0021); /* TYPE: SRV (33) */
    write_u16(resp_buf, &rpos, 0x8001); /* CLASS: IN + Cache-Flush */
    write_u32(resp_buf, &rpos, DNSSD_TTL_SRV);

    size_t srv_rdlen_pos = rpos;
    rpos += 2;
    size_t srv_rdata_start = rpos;
    write_u16(resp_buf, &rpos, 0x0000); /* Priority 0 */
    write_u16(resp_buf, &rpos, 0x0000); /* Weight 0 */
    write_u16(resp_buf, &rpos, svc->port);
    write_hostname_fqdn(resp_buf, &rpos, svc->hostname);
    uint16_t srv_rdlen = (uint16_t)(rpos - srv_rdata_start);
    resp_buf[srv_rdlen_pos] = (uint8_t)(srv_rdlen >> 8U);
    resp_buf[srv_rdlen_pos + 1] = (uint8_t)(srv_rdlen & 0xFFU);

    /* 3. Additional Record 2: TXT */
    write_instance_fqdn(resp_buf, &rpos, svc->instance_name, svc->service_type);
    write_u16(resp_buf, &rpos, 0x0010); /* TYPE: TXT (16) */
    write_u16(resp_buf, &rpos, 0x8001); /* CLASS: IN + Cache-Flush */
    write_u32(resp_buf, &rpos, DNSSD_TTL_PTR);

    size_t txt_rdlen_pos = rpos;
    rpos += 2;
    size_t txt_rdata_start = rpos;
    if (svc->txt_count > 0) {
        for (size_t t = 0; t < svc->txt_count; t++) {
            write_label(resp_buf, &rpos, svc->txt_records[t]);
        }
    } else {
        resp_buf[rpos++] = 0x00; /* Empty TXT */
    }
    uint16_t txt_rdlen = (uint16_t)(rpos - txt_rdata_start);
    resp_buf[txt_rdlen_pos] = (uint8_t)(txt_rdlen >> 8U);
    resp_buf[txt_rdlen_pos + 1] = (uint8_t)(txt_rdlen & 0xFFU);

    /* 4. Additional Record 3: A (IPv4 address) */
    write_hostname_fqdn(resp_buf, &rpos, svc->hostname);
    write_u16(resp_buf, &rpos, 0x0001); /* TYPE: A (1) */
    write_u16(resp_buf, &rpos, 0x8001); /* CLASS: IN + Cache-Flush */
    write_u32(resp_buf, &rpos, DNSSD_TTL_SRV);
    write_u16(resp_buf, &rpos, 0x0004); /* RDLENGTH 4 */
    memcpy(&resp_buf[rpos], svc->ip, 4);
    rpos += 4;

    *resp_len = rpos;
    return SYN_OK;
}

SYN_Status syn_dnssd_process_query(const SYN_DnsSd *sd, const uint8_t *query_buf, size_t query_len,
                                   uint8_t *resp_buf, size_t max_resp_len, size_t *resp_len)
{
    if (sd == NULL || query_buf == NULL || resp_buf == NULL || resp_len == NULL) {
        return SYN_ERROR;
    }
    *resp_len = 0;

    if (query_len < 12) {
        return SYN_ERROR;
    }

    /* Check flags: QR bit must be 0 (Query) */
    uint16_t flags = ((uint16_t)query_buf[2] << 8U) | query_buf[3];
    if ((flags & 0x8000U) != 0) {
        return SYN_ERROR;
    }

    uint16_t qdcount = ((uint16_t)query_buf[4] << 8U) | query_buf[5];
    if (qdcount == 0) {
        return SYN_ERROR;
    }

    size_t pos = 12;
    const SYN_DnsSd_Service *matched_svc = NULL;

    for (uint16_t q = 0; q < qdcount; q++) {
        size_t name_start = pos;
        for (size_t s = 0; s < sd->service_count; s++) {
            size_t temp_pos = name_start;
            if (match_qname_service(query_buf, query_len, &temp_pos,
                                    sd->services[s].service_type)) {
                matched_svc = &sd->services[s];
                pos = temp_pos;
                break;
            }
        }
        if (matched_svc != NULL) {
            break;
        }
        /* Skip unknown QNAME */
        while (pos < query_len && query_buf[pos] != 0) {
            pos += 1 + query_buf[pos];
        }
        if (pos < query_len && query_buf[pos] == 0) {
            pos++;
        }
        pos += 4; /* Skip QTYPE (2) + QCLASS (2) */
    }

    if (matched_svc == NULL) {
        return SYN_NOT_FOUND;
    }

    return pack_service_response(matched_svc, resp_buf, max_resp_len, resp_len);
}

SYN_Status syn_dnssd_announce(const SYN_DnsSd *sd, size_t service_index, uint8_t *resp_buf,
                              size_t max_resp_len, size_t *resp_len)
{
    if (sd == NULL || resp_buf == NULL || resp_len == NULL || service_index >= sd->service_count) {
        return SYN_ERROR;
    }

    SYN_Status st =
        pack_service_response(&sd->services[service_index], resp_buf, max_resp_len, resp_len);
    if (st == SYN_OK && sd->sock != SYN_SOCKET_INVALID && *resp_len > 0) {
        SYN_SockAddr mcast_dest;
        mcast_dest.ip[0] = 224;
        mcast_dest.ip[1] = 0;
        mcast_dest.ip[2] = 0;
        mcast_dest.ip[3] = 251;
        mcast_dest.port = SYN_DNSSD_PORT;
        syn_port_udp_sendto(sd->sock, resp_buf, *resp_len, &mcast_dest);
    }
    return st;
}

SYN_Status syn_dnssd_build_query(const char *service_type, uint8_t *query_buf, size_t max_len,
                                 size_t *query_len)
{
    if (service_type == NULL || query_buf == NULL || query_len == NULL) {
        return SYN_ERROR;
    }
    if (max_len < 128) {
        return SYN_ERROR;
    }

    size_t pos = 0;
    /* Header: ID=0, Flags=0x0000 (Query), QDCOUNT=1, ANCOUNT=0, NSCOUNT=0, ARCOUNT=0 */
    write_u16(query_buf, &pos, 0x0000);
    write_u16(query_buf, &pos, 0x0000);
    write_u16(query_buf, &pos, 0x0001); /* QDCOUNT */
    write_u16(query_buf, &pos, 0x0000);
    write_u16(query_buf, &pos, 0x0000);
    write_u16(query_buf, &pos, 0x0000);

    /* Question: QNAME = _service._proto.local, QTYPE = PTR (12), QCLASS = IN (1) */
    write_service_type_name(query_buf, &pos, service_type);
    write_u16(query_buf, &pos, 0x000C); /* PTR */
    write_u16(query_buf, &pos, 0x0001); /* IN */

    *query_len = pos;
    return SYN_OK;
}

static void skip_or_read_name(const uint8_t *buf, size_t buf_len, size_t *pos, char *out,
                              size_t max_out)
{
    size_t p = *pos;
    size_t o = 0;
    if (out != NULL && max_out > 0) {
        out[0] = '\0';
    }

    while (p < buf_len) {
        uint8_t len = buf[p];
        if (len == 0) {
            p++;
            break;
        }
        if ((len & 0xC0U) == 0xC0U) {
            p += 2;
            break;
        }
        p++;
        if (p + len > buf_len) {
            break;
        }
        if (out != NULL && max_out > 0) {
            if (o > 0 && o + 1 < max_out) {
                out[o++] = '.';
            }
            size_t copy_len = len < (max_out - 1 - o) ? len : (max_out - 1 - o);
            memcpy(&out[o], &buf[p], copy_len);
            o += copy_len;
            out[o] = '\0';
        }
        p += len;
    }
    *pos = p;
}

SYN_Status syn_dnssd_parse_response(const uint8_t *resp_buf, size_t resp_len,
                                    SYN_DnsSd_Discovered *out_disc)
{
    if (resp_buf == NULL || out_disc == NULL || resp_len < 12) {
        return SYN_ERROR;
    }
    memset(out_disc, 0, sizeof(*out_disc));

    uint16_t flags = ((uint16_t)resp_buf[2] << 8U) | resp_buf[3];
    if ((flags & 0x8000U) == 0) {
        return SYN_ERROR; /* Not a response */
    }

    uint16_t ancount = ((uint16_t)resp_buf[6] << 8U) | resp_buf[7];
    uint16_t arcount = ((uint16_t)resp_buf[10] << 8U) | resp_buf[11];
    uint16_t total_rr = ancount + arcount;

    if (total_rr == 0) {
        return SYN_ERROR;
    }

    size_t pos = 12;

    /* Parse Resource Records */
    for (uint16_t i = 0; i < total_rr && pos < resp_len; i++) {
        char rr_name[128] = {0};
        skip_or_read_name(resp_buf, resp_len, &pos, rr_name, sizeof(rr_name));

        if (pos + 10 > resp_len) {
            break;
        }
        uint16_t type = ((uint16_t)resp_buf[pos] << 8U) | resp_buf[pos + 1];
        /* skip class (2), ttl (4) */
        uint16_t rdlength = ((uint16_t)resp_buf[pos + 8] << 8U) | resp_buf[pos + 9];
        pos += 10;

        if (pos + rdlength > resp_len) {
            break;
        }

        size_t rdata_start = pos;
        if (type == 0x000C) { /* PTR */
            char target_fqdn[128] = {0};
            size_t p_rd = rdata_start;
            skip_or_read_name(resp_buf, resp_len, &p_rd, target_fqdn, sizeof(target_fqdn));
            /* Extract instance name (first label) */
            char *first_dot = strchr(target_fqdn, '.');
            if (first_dot != NULL) {
                size_t inst_len = (size_t)(first_dot - target_fqdn);
                if (inst_len < sizeof(out_disc->instance_name)) {
                    memcpy(out_disc->instance_name, target_fqdn, inst_len);
                    out_disc->instance_name[inst_len] = '\0';
                }
                /* Service type is remainder without .local */
                strncpy(out_disc->service_type, first_dot + 1, sizeof(out_disc->service_type) - 1);
                char *loc = strstr(out_disc->service_type, ".local");
                if (loc != NULL) {
                    *loc = '\0';
                }
            }
        } else if (type == 0x0021 && rdlength >= 6) { /* SRV */
            out_disc->port =
                ((uint16_t)resp_buf[rdata_start + 4] << 8U) | resp_buf[rdata_start + 5];
            size_t p_host = rdata_start + 6;
            char host_fqdn[128] = {0};
            skip_or_read_name(resp_buf, resp_len, &p_host, host_fqdn, sizeof(host_fqdn));
            char *first_dot = strchr(host_fqdn, '.');
            if (first_dot != NULL) {
                *first_dot = '\0';
            }
            strncpy(out_disc->hostname, host_fqdn, sizeof(out_disc->hostname) - 1);
        } else if (type == 0x0010 && rdlength > 0) { /* TXT */
            uint8_t txt_len = resp_buf[rdata_start];
            if (txt_len > 0 && txt_len < sizeof(out_disc->txt) && (size_t)txt_len < rdlength) {
                memcpy(out_disc->txt, &resp_buf[rdata_start + 1], txt_len);
                out_disc->txt[txt_len] = '\0';
            }
        } else if (type == 0x0001 && rdlength == 4) { /* A */
            memcpy(out_disc->ip, &resp_buf[rdata_start], 4);
        }

        pos = rdata_start + rdlength;
    }

    return SYN_OK;
}

SYN_Status syn_dnssd_discover(const SYN_DnsSd *sd, const char *service_type)
{
    if (sd == NULL || service_type == NULL || sd->sock == SYN_SOCKET_INVALID) {
        return SYN_ERROR;
    }

    uint8_t query_buf[256];
    size_t query_len = 0;
    (void)syn_dnssd_build_query(service_type, query_buf, sizeof(query_buf), &query_len);

    SYN_SockAddr mcast_dest;
    mcast_dest.ip[0] = 224;
    mcast_dest.ip[1] = 0;
    mcast_dest.ip[2] = 0;
    mcast_dest.ip[3] = 251;
    mcast_dest.port = SYN_DNSSD_PORT;

    return (syn_port_udp_sendto(sd->sock, query_buf, query_len, &mcast_dest) > 0) ? SYN_OK
                                                                                  : SYN_ERROR;
}

SYN_Status syn_dnssd_browser_init(SYN_DnsSd_Browser *browser, const SYN_DnsSd *sd,
                                  const char *service_type, SYN_DnsSd_DiscoverCallback cb,
                                  void *user_data, uint32_t timeout_ms)
{
    if (browser == NULL || sd == NULL || service_type == NULL || cb == NULL) {
        return SYN_ERROR;
    }
    memset(browser, 0, sizeof(*browser));
    browser->sd = sd;
    browser->service_type = service_type;
    browser->callback = cb;
    browser->user_data = user_data;
    browser->timeout_ms = timeout_ms;
    browser->start_time = syn_port_get_tick_ms();
    browser->active = true;

    return syn_dnssd_discover(sd, service_type);
}

SYN_PT_Status syn_dnssd_browse_task(SYN_PT *pt, SYN_Task *task)
{
    SYN_DnsSd_Browser *browser = (SYN_DnsSd_Browser *)task->user_data;
    SYN_ASSERT(browser != NULL);
    SYN_ASSERT(browser->sd != NULL);

    PT_BEGIN(pt);

    while (browser->active) {
        uint32_t now = syn_port_get_tick_ms();
        if (browser->timeout_ms > 0 && (now - browser->start_time) >= browser->timeout_ms) {
            browser->active = false;
            break;
        }

        if (browser->sd->sock != SYN_SOCKET_INVALID && syn_port_udp_readable(browser->sd->sock)) {
            uint8_t buf[1024];
            SYN_SockAddr from;
            int n = syn_port_udp_recvfrom(browser->sd->sock, buf, sizeof(buf), &from, 0);
            if (n > 12) {
                SYN_DnsSd_Discovered disc;
                if (syn_dnssd_parse_response(buf, (size_t)n, &disc) == SYN_OK) {
                    if (disc.instance_name[0] != '\0' &&
                        (browser->service_type[0] == '\0' ||
                         strcmp(disc.service_type, browser->service_type) == 0)) {
                        browser->discovered_count++;
                        if (browser->callback != NULL) {
                            browser->callback(&disc, browser->user_data);
                        }
                    }
                }
            }
        }

        PT_WAIT_UNTIL(pt,
                      !browser->active ||
                          (browser->timeout_ms > 0 &&
                           (syn_port_get_tick_ms() - browser->start_time) >= browser->timeout_ms) ||
                          syn_port_udp_readable(browser->sd->sock));
    }

    PT_END(pt);
}

SYN_PT_Status syn_dnssd_task(SYN_PT *pt, SYN_Task *task)
{
    const SYN_DnsSd *sd = (const SYN_DnsSd *)task->user_data;
    SYN_ASSERT(sd != NULL);

    PT_BEGIN(pt);

    while (1) {
        if (sd->sock != SYN_SOCKET_INVALID) {
            uint8_t query[512];
            SYN_SockAddr from;
            int n = syn_port_udp_recvfrom(sd->sock, query, sizeof(query), &from, 0);
            if (n > 0) {
                uint8_t resp[1024];
                size_t resp_len = 0;
                if (syn_dnssd_process_query(sd, query, (size_t)n, resp, sizeof(resp), &resp_len) ==
                        SYN_OK &&
                    resp_len > 0) {
                    SYN_SockAddr mcast_dest;
                    mcast_dest.ip[0] = 224;
                    mcast_dest.ip[1] = 0;
                    mcast_dest.ip[2] = 0;
                    mcast_dest.ip[3] = 251;
                    mcast_dest.port = SYN_DNSSD_PORT;
                    syn_port_udp_sendto(sd->sock, resp, resp_len, &mcast_dest);
                }
            }
        }
        PT_WAIT_UNTIL(pt, sd->sock == SYN_SOCKET_INVALID || syn_port_udp_readable(sd->sock));
    }

    PT_END(pt); /* LCOV_EXCL_LINE: Infinite task loop end */
}