diff options
author | David Voit <david.voit@gmail.com> | 2024-04-03 07:24:48 +0200 |
---|---|---|
committer | Pavel Shilovsky <pshilovsky@samba.org> | 2024-04-28 00:00:12 +0000 |
commit | c6bf4d9a59809fbb0c22ef9eb167c099ab8089fb (patch) | |
tree | 9086346733731671d164c73131f1d3467d7022c6 /cldap_ping.c | |
parent | 4718e09e4b15b957bf9d729793bc3de7caad8134 (diff) | |
download | cifs-utils-for-next.tar.gz cifs-utils-for-next.tar.bz2 cifs-utils-for-next.zip |
Implement CLDAP Ping to find the closest sitefor-next
For domain based DFS we always need to contact the domain controllers.
On setups, which are using bigger AD installations you could get random dc on the other side of the world,
if you don't have site support. This can lead to network timeouts and other problems.
CLDAP-Ping uses ASN.1 + UDP (CLDAP) and custom-DCE encoding including DName compressions without
field separation. Finally after finding the sitename we now need to do a DNS SRV lookups to find
the correct IPs to our closest site and fill up the remaining IPs from the global list.
Reviewed-by: Paulo Alcantara (Red Hat) <pc@manguebit.com>
Reviewed-by: Jacob Shivers <jshivers@redhat.com>
Signed-off-by: David Voit <david.voit@gmail.com>
Diffstat (limited to 'cldap_ping.c')
-rw-r--r-- | cldap_ping.c | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/cldap_ping.c b/cldap_ping.c new file mode 100644 index 0000000..9183b27 --- /dev/null +++ b/cldap_ping.c @@ -0,0 +1,345 @@ +/* + * CLDAP Ping to find closest ClientSiteName + * + * Copyright (C) 2024 David Voit (david.voit@gmail.com) + * + * 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; either version 3 of the License, or + * (at your option) any later version. + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +#include <talloc.h> +#include <string.h> +#include <sys/socket.h> +#include <arpa/inet.h> +#include <unistd.h> +#include <resolv.h> +#include <stdbool.h> +#include "data_blob.h" +#include "asn1.h" +#include "cldap_ping.h" + +#define LDAP_DNS_DOMAIN "DnsDomain" +#define LDAP_DNS_DOMAIN_LEN strlen(LDAP_DNS_DOMAIN) +#define LDAP_NT_VERSION "NtVer" +#define LDAP_NT_VERSION_LEN strlen(LDAP_NT_VERSION) +#define LDAP_ATTRIBUTE_NETLOGON "NetLogon" +#define LDAP_ATTRIBUTE_NETLOGON_LEN strlen(LDAP_ATTRIBUTE_NETLOGON) + + +// Parse a ASN.1 BER tag size-field, returns start of payload of tag +char *parse_ber_size(char *buf, size_t *tag_size) { + size_t size = *buf & 0xff; + char *ret = (buf + 1); + if (size >= 0x81) { + switch (size) { + case 0x81: + size = *ret & 0xff; + ret += 1; + break; + case 0x82: + size = (*ret << 8) | (*(ret + 1) & 0xff); + ret += 2; + break; + case 0x83: + size = (*ret << 16) | (*(ret + 1) << 8) | (*(ret + 2) & 0xff); + ret += 3; + break; + case 0x84: + size = (*ret << 24) | (*(ret + 1) << 16) | (*(ret + 2) << 8) | (*(ret + 3) & 0xff); + ret += 4; + break; + default: + return NULL; + } + } + + *tag_size = size; + return ret; +} + +// simple wrapper over dn_expand which also calculates the new offset for the next compressed dn +int read_dns_string(char *buf, size_t buf_size, char *dest, size_t dest_size, size_t *offset) { + int compressed_length = dn_expand((u_char *)buf, (u_char *)buf+buf_size, (u_char *)buf + *offset, dest, (int)dest_size); + if (compressed_length < 0) { + return -1; + } + + *offset = *offset+compressed_length; + + return 0; +} + +// LDAP request for: (&(DnsDomain=DOMAIN_HERE)(NtVer=\\06\\00\\00\\00)) +ASN1_DATA *generate_cldap_query(char *domain) { + ASN1_DATA *data; + TALLOC_CTX *mem_ctx = talloc_init("cldap"); + + data = asn1_init(mem_ctx); + asn1_push_tag(data, ASN1_SEQUENCE(0)); + + // Message id + asn1_push_tag(data, ASN1_INTEGER); + asn1_write_uint8(data, 1); + asn1_pop_tag(data); + + // SearchRequest + asn1_push_tag(data, ASN1_APPLICATION(3)); + + // empty baseObject + asn1_push_tag(data, ASN1_OCTET_STRING); + asn1_pop_tag(data); + + // scope 0 = baseObject + asn1_push_tag(data, ASN1_ENUMERATED); + asn1_write_uint8(data, 0); + asn1_pop_tag(data); + + // derefAliasses 0=neverDerefAlias + asn1_push_tag(data, ASN1_ENUMERATED); + asn1_write_uint8(data, 0); + asn1_pop_tag(data); + + // sizeLimit + asn1_push_tag(data, ASN1_INTEGER); + asn1_write_uint8(data, 0); + asn1_pop_tag(data); + + // timeLimit + asn1_push_tag(data, ASN1_INTEGER); + asn1_write_uint8(data, 0); + asn1_pop_tag(data); + + // typesOnly + asn1_push_tag(data, ASN1_BOOLEAN); + asn1_write_uint8(data, 0); + asn1_pop_tag(data); + + // AND + asn1_push_tag(data, ASN1_CONTEXT(0)); + // equalityMatch + asn1_push_tag(data, ASN1_CONTEXT(3)); + asn1_write_OctetString(data, LDAP_DNS_DOMAIN, LDAP_DNS_DOMAIN_LEN); + asn1_write_OctetString(data, domain, strlen(domain)); + asn1_pop_tag(data); + + // equalityMatch + asn1_push_tag(data, ASN1_CONTEXT(3)); + asn1_write_OctetString(data, LDAP_NT_VERSION, LDAP_NT_VERSION_LEN); + // Bitmask NETLOGON_NT_VERSION_5 & NETLOGON_NT_VERSION_5EX -> To get NETLOGON_SAM_LOGON_RESPONSE_EX as response + asn1_write_OctetString(data, "\x06\x00\x00\x00", 4); + asn1_pop_tag(data); + + // End AND + asn1_pop_tag(data); + + asn1_push_tag(data, ASN1_SEQUENCE(0)); + asn1_write_OctetString(data, LDAP_ATTRIBUTE_NETLOGON, LDAP_ATTRIBUTE_NETLOGON_LEN); + asn1_pop_tag(data); + + // End SearchRequest + asn1_pop_tag(data); + // End Sequence + asn1_pop_tag(data); + + return data; +} + +// Input is a cldap response, output is a pointer to the NETLOGON_SAM_LOGON_RESPONSE_EX payload +ssize_t extract_netlogon_section(char *buffer, size_t buffer_size, char **netlogon_payload) { + size_t ber_size; + size_t netlogon_payload_size; + // Not enough space to read initial sequence - not an correct cldap response + if (buffer_size < 7) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + // Sequence tag + if (*buffer != 0x30) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + char *message_id_tag = parse_ber_size(buffer + 1, &ber_size); + + if (ber_size > buffer_size) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + if (*message_id_tag != 0x02) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + char *message_id = parse_ber_size(message_id_tag + 1, &ber_size); + + if (ber_size != 1 || *message_id != 1) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + // SearchResultEntry + if (*(message_id+1) != 0x64) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + char *object_name_tag = parse_ber_size(message_id+2, &ber_size); + if (object_name_tag == NULL) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + char *object_name = parse_ber_size(object_name_tag+1, &ber_size); + if (object_name == NULL) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + if (*object_name_tag != 4 || ber_size != 0) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + char *partial_attribute_list_tag = parse_ber_size(object_name+1, &ber_size); + if (partial_attribute_list_tag == NULL) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + if (*partial_attribute_list_tag != 0x30) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + + char *partial_attribute_tag = parse_ber_size(partial_attribute_list_tag+1, &ber_size); + if (partial_attribute_tag == NULL) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + char *attribute_name = parse_ber_size(partial_attribute_tag+1, &ber_size); + if (attribute_name == NULL) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + if (ber_size != LDAP_ATTRIBUTE_NETLOGON_LEN) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + if (strncasecmp(LDAP_ATTRIBUTE_NETLOGON, attribute_name, LDAP_ATTRIBUTE_NETLOGON_LEN) != 0) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + // SET + if (*(attribute_name+LDAP_ATTRIBUTE_NETLOGON_LEN) != 0x31) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + char *start_of_data = parse_ber_size(attribute_name+LDAP_ATTRIBUTE_NETLOGON_LEN+1, &ber_size); + if (start_of_data == NULL) { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + // octat-string of NetLogon data + if (*start_of_data != '\x04') { + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + *netlogon_payload = parse_ber_size(start_of_data + 1, &netlogon_payload_size); + + if (*netlogon_payload == NULL) { + *netlogon_payload = NULL; + return CLDAP_PING_PARSE_ERROR_LDAP; + } + + return (ssize_t)netlogon_payload_size; +} + +int netlogon_get_client_site(char *netlogon_response, size_t netlogon_size, char *sitename) { + // 24 mandatory bytes + if (netlogon_size < 25) { + return CLDAP_PING_PARSE_ERROR_NETLOGON; + } + + // LOGON_SAM_PAUSE_RESPONSE_EX -> Netlogon service is not in-sync try next dc instead + if (*netlogon_response == 0x18 && *(netlogon_response + 1) == 0x00) { + return CLDAP_PING_TRYNEXT; + } + + // NETLOGON_SAM_LOGON_RESPONSE_EX Opcode: 0x17 + if (*netlogon_response != 0x17 || *(netlogon_response + 1) != 0x00) { + return CLDAP_PING_PARSE_ERROR_NETLOGON; + } + + // skip over sbz, ds_flags and domain_guid + // and start directly at variable string portion of NETLOGON_SAM_LOGON_RESPONSE_EX + size_t offset = 24; + + for (int i=0; i < 8; i++) { + // iterate over DnsForestName, DnsDomainName, NetbiosDomainName, NetbiosComputerName, UserName, DcSiteName + // to finally get to our desired ClientSiteName field + if (read_dns_string(netlogon_response, netlogon_size, sitename, MAXCDNAME, &offset) < 0) { + return CLDAP_PING_PARSE_ERROR_NETLOGON; + } + } + + return 0; +} + +int cldap_ping(char *domain, sa_family_t family, void *addr, char *site_name) { + char buffer[8196]; + ssize_t response_size; + char *netlogon_response; + ssize_t netlogon_size; + struct sockaddr_storage socketaddr; + size_t addr_size; + int sock = socket(family, SOCK_DGRAM, 0); + if (sock < 0) { + return CLDAP_PING_NETWORK_ERROR; + } + + ASN1_DATA *data = generate_cldap_query(domain); + + if (family == AF_INET6) { + addr_size = sizeof(struct sockaddr_in6); + bzero((void *) &socketaddr, addr_size); + socketaddr.ss_family = AF_INET6; + ((struct sockaddr_in6 *)&socketaddr)->sin6_addr = *((struct in6_addr*)addr); + ((struct sockaddr_in6 *)&socketaddr)->sin6_port = htons(389); + } else { + addr_size = sizeof(struct sockaddr_in); + bzero((void *) &socketaddr, addr_size); + socketaddr.ss_family = AF_INET; + ((struct sockaddr_in *)&socketaddr)->sin_addr = *((struct in_addr*)addr); + ((struct sockaddr_in *)&socketaddr)->sin_port = htons(389); + } + + struct timeval timeout = {.tv_sec = 2, .tv_usec = 0}; + if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) { + return CLDAP_PING_NETWORK_ERROR; + } + if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { + return CLDAP_PING_NETWORK_ERROR; + } + + if (sendto(sock, data->data, data->length, 0, (struct sockaddr *)&socketaddr, addr_size) < 0) { + close(sock); + return CLDAP_PING_TRYNEXT; + } + + asn1_free(data); + response_size = recv(sock, buffer, sizeof(buffer), 0); + close(sock); + + if (response_size < 0) { + return CLDAP_PING_TRYNEXT; + } + + netlogon_size = extract_netlogon_section(buffer, response_size, &netlogon_response); + if (netlogon_size < 0) { + return (int)netlogon_size; + } + + return netlogon_get_client_site(netlogon_response, netlogon_size, site_name); +} |