Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

iOS : Fixing DNS TXT Record Lookup for Apple Caching Server Discovery in iOS

When I was building my iOS application to automatically discover Apple caching servers on a network, I encountered several critical issues that prevented DNS TXT record lookups from working properly. This post covers the technical problems I faced and how I resolved them to create a working solution that can automatically discover multiple caching servers using just a domain name.

The Problem: DNS Queries That Never Complete

The original implementation appeared correct at first glance - it called DNSServiceQueryRecord to query for TXT records and set up a callback function. However, the callback never fired, and DNS queries would timeout after 10 seconds.

// This approach was fundamentally flawed
let result = DNSServiceQueryRecord(&serviceRef, 0, interfaceIndex, domain, 
                                 UInt16(kDNSServiceType_TXT), UInt16(kDNSServiceClass_IN), 
                                 callback, context)
DNSServiceProcessResult(serviceRef.pointee) // This blocks and fails

Root Cause Analysis

After extensive research, I identified three critical issues:

1. Incorrect Socket Handling

The biggest problem was calling DNSServiceProcessResult only once. This function needs to be called repeatedly whenever the underlying socket becomes readable, not just once on a background queue.

2. Missing Build Configuration

iOS requires the -lresolv linker flag for DNS queries to function properly. Without this flag, DNS operations fail silently or return immediate errors.

3. Incomplete Callback Implementation

The C callback function wasn't properly implemented to parse TXT record data and handle multiple IP addresses.

The Solution: Asynchronous DNS Handling

Step 1: Add Required Build Settings

First, I configured Xcode to link against the resolver library:

  1. Select the project in the navigator
  2. Select the app target
  3. Click "Build Settings" tab
  4. Search for "Other Linker Flags"
  5. Add: -lresolv

Step 2: Implement Proper Socket Monitoring

Instead of calling DNSServiceProcessResult once, I used DispatchSource to monitor the DNS socket:

let sockfd = DNSServiceRefSockFD(serviceRef)
let source = DispatchSource.makeReadSource(fileDescriptor: sockfd, queue: DispatchQueue.global())

source.setEventHandler {
    DNSServiceProcessResult(serviceRef) // Called when socket is readable
}

source.setCancelHandler {
    DNSServiceRefDeallocate(serviceRef)
}

source.resume()

Step 3: Implement Callback Function

The callback needed to properly parse TXT record data, which uses length-prefixed strings:

let dnsQueryCallback: DNSServiceQueryRecordReply = { (sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdlen, rdata, ttl, context) -> Void in
    
    guard errorCode == kDNSServiceErr_NoError else { return }
    guard let rdata = rdata?.assumingMemoryBound(to: UInt8.self), rdlen > 0 else { return }
    
    // Parse length-prefixed TXT record strings
    var offset = 0
    var txtRecords: [String] = []
    
    while offset < rdlen {
        let length = Int(rdata[offset])
        offset += 1
        
        if length > 0 && offset + length <= rdlen {
            let data = Data(bytes: rdata + offset, count: length)
            if let record = String(data: data, encoding: .utf8) {
                txtRecords.append(record)
            }
            offset += length
        }
    }
    
    // Process found IP addresses
    for record in txtRecords {
        let trimmedRecord = record.trimmingCharacters(in: .whitespacesAndNewlines)
        if isValidIPAddress(trimmedRecord) {
            globalFoundIPs.append(trimmedRecord)
        }
    }
}

Step 4: Handle Multiple IP Addresses

The solution automatically processes multiple IP addresses from the TXT record:

// After DNS completes, test each found IP
for ip in globalFoundIPs {
    let commonPorts = [49152, 49159, 49200, 49597]
    
    for port in commonPorts {
        if try await testConnection(ip: ip, port: port) {
            let server = CachingServer(ipAddress: ip, port: port, isActive: true)
            servers.append(server)
            break // Found working port, move to next IP
        }
    }
}

DNS Record Configuration

The system queries for TXT records using the naming convention: _internal-cache-servers._tcp.<domain>

The TXT record should contain the IP addresses of caching servers:

  • Record Name: _internal-cache-servers._tcp
  • Record Type: TXT
  • Record Value: 192.168.1.100 (or multiple IPs as separate strings)

How It Works End-to-End

  1. DNS Query: The app queries for _internal-cache-servers._tcp.<domain> TXT records
  2. Socket Monitoring: DispatchSource monitors the DNS socket for responses
  3. Record Parsing: The callback parses TXT record data to extract IP addresses
  4. Server Testing: Each IP is tested on common caching server ports (49152, 49159, 49200, 49597)
  5. Verification: HTTP requests verify servers are actually Apple caching servers by checking response headers
  6. Results: Discovered servers are presented to the user with connection status
Previous Post Next Post

نموذج الاتصال