// SPDX-License-Identifier: BSD-3-Clause OR Apache-2.0 #include "ud3tn/common.h" #include "ud3tn/eid.h" #include #include #include #include #include #include #include #include static const struct eid INVALID_EID = (struct eid){ .scheme = EID_SCHEME_UNKNOWN }; static char *eidstr_dtn_with_slash(const char *const eid_str, const size_t eid_len) { char *const terminated_eid = malloc(eid_len + 2); if (terminated_eid == NULL) return NULL; memcpy(terminated_eid, eid_str, eid_len); terminated_eid[eid_len] = '/'; terminated_eid[eid_len + 1] = '\0'; return terminated_eid; } static char *eidstr_dtn_normalize(const char *const eid_str) { const size_t eid_len = strlen(eid_str); if (eid_len < 7 || memcmp(eid_str, "dtn://", 6) != 0) return NULL; const char *const delim = strchr((char *)&eid_str[6], '/'); // There is no slash -> terminate it with a slash to be consistent! if (!delim) return eidstr_dtn_with_slash(eid_str, eid_len); return strdup(eid_str); } struct eid eid_from_string(const char *const eid_str) { struct eid return_value = INVALID_EID; // Note that `get_eid_scheme` also checks the length to be <= EID_MAX_LEN. switch (eidstr_get_eid_scheme(eid_str)) { case EID_SCHEME_DTN: return_value.scheme = EID_SCHEME_DTN; return_value.dtn_eid = eidstr_dtn_normalize(eid_str); if (!return_value.dtn_eid && strcmp(eid_str, "dtn:none") == 0) { break; // OK, keep NULL } else if (!eidstr_dtn_is_valid(return_value.dtn_eid)) { // NOTE that this case also applies if return_value.dtn_eid == NULL (Fail) free(return_value.dtn_eid); return INVALID_EID; } break; case EID_SCHEME_IPN: return_value.scheme = EID_SCHEME_IPN; if (!eidstr_ipn_is_valid(eid_str, &return_value.ipn_eid.fully_qualified_node_number, &return_value.ipn_eid.service_number)) return INVALID_EID; break; case EID_SCHEME_UNKNOWN: return INVALID_EID; default: ASSERT(false); // Bug: Scheme not handled! return INVALID_EID; } return return_value; } // This includes the `case EID_SCHEME_IPN` in the switch statement in `eid_to_string`. static inline size_t decimal_digits(uint64_t number) { size_t digits = 0; if (number == 0) return 1; while (number != 0) { digits++; number /= 10; } return digits; } char *eid_to_string(const struct eid eid) { return eid_to_string2(eid, (bool)(EID_IPN_FORCE_2_ELEMENT)); } char *eid_to_string2(const struct eid eid, const bool force_ipn_2_element) { if (eid.scheme == EID_SCHEME_DTN) { if (eid.dtn_eid) return strdup(eid.dtn_eid); else return strdup("dtn:none"); } else if (eid.scheme == EID_SCHEME_IPN && (ipn_eid_allocator(eid.ipn_eid) == 0 || force_ipn_2_element)) { const bool localnode = eid_is_localnode(eid); const size_t fqnn_digits = ( localnode ? 1 : decimal_digits(eid.ipn_eid.fully_qualified_node_number) ); // TODO: Consider always allocating 46 bytes or caching len/value. // "ipn:" + nodenum + "." + servicenum + "\0" const size_t ipn_len = ( 4 + fqnn_digits + 1 + decimal_digits(eid.ipn_eid.service_number) + 1 ); // "ipn:18446744073709551615.18446744073709551615\0" ASSERT(ipn_len <= 20 + 20 + 6); char *const ipn_eid = malloc(ipn_len); if (ipn_eid == NULL) return NULL; int written; if (localnode) { written = snprintf( ipn_eid, ipn_len, "ipn:!.%" PRIu64, eid.ipn_eid.service_number ); ASSERT(written + 1 == (ssize_t)ipn_len); } else { written = snprintf( ipn_eid, ipn_len, "ipn:%" PRIu64 ".%" PRIu64, eid.ipn_eid.fully_qualified_node_number, eid.ipn_eid.service_number ); ASSERT(written + 1 == (ssize_t)ipn_len); } return ipn_eid; } else if (eid.scheme == EID_SCHEME_IPN) { // ipn 3-element format // "ipn:" + alloc + "." + node + "." + servicenum + "\0" const size_t ipn_len = ( 4 + decimal_digits(ipn_eid_allocator(eid.ipn_eid)) + 1 + decimal_digits(ipn_eid_node(eid.ipn_eid)) + 1 + decimal_digits(eid.ipn_eid.service_number) + 1 ); // "ipn:4294967295.4294967295.18446744073709551615\0" ASSERT(ipn_len <= 10 + 10 + 20 + 7); char *const ipn_eid = malloc(ipn_len); if (ipn_eid == NULL) return NULL; const int written = snprintf( ipn_eid, ipn_len, "ipn:%" PRIu32 ".%" PRIu32 ".%" PRIu64, ipn_eid_allocator(eid.ipn_eid), ipn_eid_node(eid.ipn_eid), eid.ipn_eid.service_number ); ASSERT(written + 1 == (ssize_t)ipn_len); return ipn_eid; } else if (eid.scheme == EID_SCHEME_UNKNOWN) { return NULL; } ASSERT(false); // Bug: Scheme not handled! return NULL; } void eid_free(struct eid eid) { if (eid.scheme == EID_SCHEME_DTN) free(eid.dtn_eid); // NOP if NULL } struct eid eid_dup(const struct eid eid) { struct eid dup = eid; if (eid.scheme == EID_SCHEME_DTN && eid.dtn_eid != NULL) { dup.dtn_eid = strdup(eid.dtn_eid); // Allocation failure. if (dup.dtn_eid == NULL && eid.dtn_eid != NULL) return INVALID_EID; } return dup; } bool eid_eq(const struct eid eid1, const struct eid eid2) { if (eid_is_error(eid1) || eid_is_error(eid2)) return false; // Different representations of the Null Endpoint are equal. if (eid_is_null(eid1) && eid_is_null(eid2)) return true; if (eid1.scheme != eid2.scheme) return false; switch (eid1.scheme) { case EID_SCHEME_DTN: if (eid1.dtn_eid == NULL || eid2.dtn_eid == NULL) return eid1.dtn_eid == NULL && eid2.dtn_eid == NULL; return !strcmp(eid1.dtn_eid, eid2.dtn_eid); case EID_SCHEME_IPN: return ( ( eid1.ipn_eid.fully_qualified_node_number == eid2.ipn_eid.fully_qualified_node_number ) && eid1.ipn_eid.service_number == eid2.ipn_eid.service_number ); case EID_SCHEME_UNKNOWN: return false; } ASSERT(false); // Bug: Scheme not handled! return false; } bool eid_node_eq(const struct eid eid1, const struct eid eid2) { if (eid_is_error(eid1) || eid_is_error(eid2)) return false; // Different representations of the Null Endpoint are the same "node" -- none. // We could consider treating this differently, e.g., by defining that this function // always returns false if any eid is the Null Endpoint, but I consider this to be only // more complex at the moment. -> Reconsider if there is a reason to do it. if (eid_is_null(eid1) && eid_is_null(eid2)) return true; if (eid1.scheme != eid2.scheme) return false; switch (eid1.scheme) { case EID_SCHEME_DTN: // Same pointer (incl. NULL) -> equal. if (eid1.dtn_eid == eid2.dtn_eid) return true; const size_t eid1_len = strlen(eid1.dtn_eid); const size_t eid2_len = strlen(eid2.dtn_eid); // Too short -> invalid. if (eid1_len < 7 || eid2_len < 7) return false; const char *eid1_delim = strchr((char *)&eid1.dtn_eid[6], '/'); const char *eid2_delim = strchr((char *)&eid2.dtn_eid[6], '/'); const size_t node1_len = ( eid1_delim ? (size_t)(eid1_delim - eid1.dtn_eid) : eid1_len ); const size_t node2_len = ( eid2_delim ? (size_t)(eid2_delim - eid2.dtn_eid) : eid2_len ); // Length of node part is different if (node1_len != node2_len) return false; // Compare node part return memcmp(eid1.dtn_eid, eid2.dtn_eid, node1_len) == 0; case EID_SCHEME_IPN: return ( eid1.ipn_eid.fully_qualified_node_number == eid2.ipn_eid.fully_qualified_node_number ); case EID_SCHEME_UNKNOWN: return false; } ASSERT(false); // Bug: Scheme not handled! return false; } static char *get_dtn_admin_endpoint(const char *const dtn_eid) { const size_t eid_len = strlen(dtn_eid); // Invalid EID struct (may be a bug): too short or scheme invalid if (eid_len < 7 || memcmp(dtn_eid, "dtn://", 6) != 0) return NULL; const char *const delim = strchr((char *)&dtn_eid[6], '/'); // No slash found; OK for compatibility with ION etc. -> append slash and return. if (!delim) return eidstr_dtn_with_slash(dtn_eid, eid_len); // Reduce length to scheme + node + delimiter (e.g. "dtn://node/") const size_t new_len = delim - dtn_eid + 1; char *const dtn_admin_ep = malloc(new_len + 1); if (dtn_admin_ep == NULL) return NULL; memcpy(dtn_admin_ep, dtn_eid, new_len); dtn_admin_ep[new_len] = '\0'; return dtn_admin_ep; } struct eid eid_get_admin_endpoint(const struct eid eid) { struct eid result_eid = INVALID_EID; switch (eid.scheme) { case EID_SCHEME_DTN: if (eid.dtn_eid == NULL) // dtn:none (-> we can simply copy by value) return eid; result_eid.dtn_eid = get_dtn_admin_endpoint(eid.dtn_eid); if (result_eid.dtn_eid != NULL) result_eid.scheme = EID_SCHEME_DTN; break; case EID_SCHEME_IPN: result_eid.scheme = EID_SCHEME_IPN, result_eid.ipn_eid = (struct ipn_eid){ .fully_qualified_node_number = eid.ipn_eid.fully_qualified_node_number, .service_number = 0, }; break; case EID_SCHEME_UNKNOWN: break; default: ASSERT(false); // Bug: Scheme not handled! } return result_eid; } struct eid eid_get_agent_eid(const struct eid node_id, const char *agent_id) { struct eid admin_ep = eid_get_admin_endpoint(node_id); if (eid_is_error(admin_ep) || eid_is_null(admin_ep)) return INVALID_EID; // `ipn` if (admin_ep.scheme == EID_SCHEME_IPN) { uint64_t agent_service_number; const char *end = eidstr_parse_ipn_ull(agent_id, &agent_service_number); if (!end || *end != '\0') { eid_free(admin_ep); return INVALID_EID; } // update admin_ep admin_ep.ipn_eid.service_number = agent_service_number; return admin_ep; } else if (admin_ep.scheme != EID_SCHEME_DTN) { eid_free(admin_ep); return INVALID_EID; // unsupported } // `dtn` if (!eidstr_dtn_demux_is_valid(agent_id)) { eid_free(admin_ep); return INVALID_EID; } const size_t prefix_len = strlen(admin_ep.dtn_eid); const size_t agent_id_len = strlen(agent_id); const size_t alloc_len = prefix_len + agent_id_len + 1; if (alloc_len > EID_MAX_LEN) { eid_free(admin_ep); return INVALID_EID; } char *const new_dtn_eid = malloc(alloc_len); if (!new_dtn_eid) { eid_free(admin_ep); return INVALID_EID; } memcpy(&new_dtn_eid[0], admin_ep.dtn_eid, prefix_len); memcpy(&new_dtn_eid[prefix_len], agent_id, agent_id_len); new_dtn_eid[prefix_len + agent_id_len] = '\0'; struct eid result = admin_ep; // copy by value & update result.dtn_eid = new_dtn_eid; eid_free(admin_ep); return result; } char *eid_get_agent_id_str(const struct eid eid) { // dtn decoding const char *delim; // ipn decoding size_t result_len; char *result; if (eid_is_error(eid) || eid_is_null(eid)) return NULL; switch (eid.scheme) { case EID_SCHEME_DTN: // Special case, e.g., "dtn:none" if (strncmp(eid.dtn_eid, "dtn://", 6)) return NULL; delim = strchr((char *)&eid.dtn_eid[6], '/'); if (!delim || delim[1] == '\0') return NULL; // NULL agent (administrative endpoint) return strdup(&delim[1]); case EID_SCHEME_IPN: if (eid.ipn_eid.service_number == 0) return NULL; // NULL agent (administrative endpoint) result_len = decimal_digits(eid.ipn_eid.service_number); result = malloc(result_len + 1); // + '\0' if (!result) return NULL; int written = snprintf( result, result_len + 1, "%" PRIu64, eid.ipn_eid.service_number ); ASSERT(written == (ssize_t)result_len); return result; case EID_SCHEME_UNKNOWN: return NULL; } ASSERT(false); // Bug: Scheme not handled! return NULL; } bool eidstr_is_valid(const char *const eid_str) { switch (eidstr_get_eid_scheme(eid_str)) { case EID_SCHEME_DTN: return eidstr_dtn_is_valid(eid_str); case EID_SCHEME_IPN: return eidstr_ipn_is_valid(eid_str, NULL, NULL); case EID_SCHEME_UNKNOWN: return false; } ASSERT(false); // Bug: Scheme not handled! return false; } // https://datatracker.ietf.org/doc/html/draft-ietf-dtn-bpbis-31#section-4.2.5.1 bool eidstr_dtn_is_valid(const char *const eid_str) { if (!eid_str) return false; const size_t len = strlen(eid_str); // minimum is dtn:none or dtn://C with C being a char if (len < 7) return false; else if (len > EID_MAX_LEN) return false; else if (len == 8 && memcmp(eid_str, "dtn:none", 8) == 0) return true; else if (memcmp(eid_str, "dtn://", 6)) return false; // check node-name const char *cur = &eid_str[6]; // after "dtn://" // this loop (also) terminates on '/' and '\0' -> checked below while (*cur) { if (!isalnum(*cur) && *cur != '-' && *cur != '.' && *cur != '_') break; cur++; } // fail if node-name is zero-length if (cur == &eid_str[6]) return false; // note that we allow dtn EID with and without trailing slash if (*cur == '\0') return true; // fail if node-name is terminated by something other than '/' if (*cur != '/') return false; // check demux return eidstr_dtn_demux_is_valid(cur); } bool eidstr_dtn_demux_is_valid(const char *demux_str) { if (!demux_str) return false; while (*demux_str) { // VCHAR is in range %x21-7E -> RFC 5234) if (*demux_str < 0x21 || *demux_str > 0x7E) return false; demux_str++; } return true; } enum eid_scheme eidstr_get_eid_scheme(const char *const eid_str) { if (!eid_str) return EID_SCHEME_UNKNOWN; const size_t len = strlen(eid_str); if (len > EID_MAX_LEN) return EID_SCHEME_UNKNOWN; if (len >= 4 && !memcmp(eid_str, "dtn:", 4)) return EID_SCHEME_DTN; else if (len >= 4 && !memcmp(eid_str, "ipn:", 4)) return EID_SCHEME_IPN; return EID_SCHEME_UNKNOWN; } const char *eidstr_parse_ipn_ull(const char *const cur, uint64_t *const out) { unsigned long long tmp; const char *next, *next_strtoull; if (!cur || *cur == '\0') return NULL; // Check that only digits are part of the substring // Otherwise, strtoull may accept things like '-' and thousands // separators according to the currently set locale. next = cur; while (*next && *next >= '0' && *next <= '9') next++; if (*next != '.' && *next != '\0') return NULL; errno = 0; tmp = strtoull(cur, (char **)&next_strtoull, 10); // strtoull must have parsed the same as the digit check above if (next != next_strtoull) return NULL; // Special case: returns 0 -> failed OR is actually zero if (tmp == 0 && (next - cur != 1 || *cur != '0')) return NULL; // Special case: overflow if (tmp == ULLONG_MAX && errno == ERANGE) return NULL; if (out) *out = (uint64_t)tmp; return next; } // https://datatracker.ietf.org/doc/html/rfc6260 // check "ipn:node.service" EID bool eidstr_ipn_is_valid( const char *const eid_str, uint64_t *const fqnn_out, uint64_t *const service_out) { if (eidstr_get_eid_scheme(eid_str) != EID_SCHEME_IPN) return false; const char *cur; uint64_t tmp_num1, tmp_num2; if (eid_str[4] == '!') { tmp_num1 = 0xFFFFFFFFUL; cur = &eid_str[5]; } else { cur = eidstr_parse_ipn_ull(&eid_str[4], &tmp_num1); } if (!cur || *cur != '.') return false; cur = eidstr_parse_ipn_ull(cur + 1, &tmp_num2); if (!cur) return false; uint64_t tmp_node, tmp_service; if (*cur == '\0') { // 2-element tmp_node = tmp_num1; tmp_service = tmp_num2; } else if (*cur == '.' && eid_str[4] != '!') { // 3-element (invalid for LocalNode EIDs starting with "!.") // allocator and node were parsed and must be <= 32 bits if (tmp_num1 > 0xFFFFFFFFUL || tmp_num2 > 0xFFFFFFFFUL) return false; tmp_node = (tmp_num1 << 32) | tmp_num2; cur = eidstr_parse_ipn_ull(cur + 1, &tmp_service); if (!cur || *cur != '\0') return false; } else { return false; } // RFC9758 3.4.1: this "[...] is invalid. Such ipn URIs MUST NOT be composed [...]" // We handle this gracefully and read them as the Null Endpoint. if (tmp_node == 0 && tmp_service != 0) tmp_service = 0; if (fqnn_out) *fqnn_out = tmp_node; if (service_out) *service_out = tmp_service; return true; }