MCDeviceDiscovery.m   [plain text]


//
//  MCDeviceDiscovery.m
//  HIDRelay
//
//  Created by Roberto Yepez on 8/5/14.
//  Copyright (c) 2014 Roberto Yepez. All rights reserved.
//

#import <TargetConditionals.h>
#import "MCDeviceDiscovery.h"

#import <SystemConfiguration/SCNetworkConfigurationPrivate.h>
#import <SystemConfiguration/SCDynamicStore.h>
#import <SystemConfiguration/SCDynamicStoreKey.h>
#import <SystemConfiguration/SCSchemaDefinitions.h>

#import <net/if.h>
#import <netdb.h>
#import <sys/ioctl.h>
#import <netinet/in.h>

char * const kHandshakingMulticastPort      = "5123";
// all nodes in link multicast address
char * const kHandshakingMulticastAddress   = "ff02::1";
// Prefix used in link local IP addresses
NSString * const kHandshakeMessagePrefix    = @"hrhs";

@interface MCDeviceDiscovery ()
{
}

// Queue for asynchronous socket sending and receiving
@property (nonatomic,strong) dispatch_queue_t handshakeWorkQueue;

// Timer to periodically send a multicast message
@property (nonatomic,strong) dispatch_source_t multicastSendTimer;

// name of local active ethernet interface (ex. @"en1")
@property (nonatomic,strong) NSString *localInterfaceName;
// IPv4 IP assigned to localInterfaceName. This is expected to be a link local address (169.254.X.X).
@property (nonatomic,strong) NSString *localIP;
// IPv4 IP assigned to remote  host. This is expected to be a link local address (169.254.X.X).
@property (nonatomic,strong) NSString *remoteIP;

// IPv6 address used for multicasting during handshaking algorithm
@property (nonatomic, assign) struct addrinfo *multicastAddr;
// IPv6 socket used for sending multicast during handshaking
@property (nonatomic, assign) int sendSocketFd;
// IPv6 socket used for receiving multicast during handshaking
@property (nonatomic, assign) int receiveSocketFd;
// Dispatch source used for asyunchronous reading
@property (nonatomic, strong) dispatch_source_t readSource;

// YES when local host knows the remoteIP and the remote host knows the localP
@property (nonatomic, assign) BOOL didCompleteHandshake;

// Block to call when finished performing handshake
@property (nonatomic, strong) MCDeviceHandshakeCompletion handshakeCompletionBlock;

@end

@implementation MCDeviceDiscovery

- (id)initWithInterface:(NSString *)interface
{
    self = [super init];
    if (self != nil) {
        self.localInterfaceName = interface;
    }
    return self;
}

#pragma mark Local Interface Info

+ (NSString *)localEthernetInterfaceName
{
    NSString * result = nil;
    SCDynamicStoreRef storeRef = SCDynamicStoreCreate(NULL,(CFStringRef)@"FindActiveEthernetInterface", NULL, NULL);

    NSString *globalPath = (__bridge_transfer NSString*)SCDynamicStoreKeyCreateNetworkGlobalEntity(kCFAllocatorDefault, kSCDynamicStoreDomainSetup, kSCEntNetIPv4);
    // equivalent to SCDynamicStoreCopyValue(storeRef, CFSTR("Setup:/Network/Global/IPv4")) but without assuming the schema structure
    NSDictionary *global = (__bridge_transfer NSDictionary*)SCDynamicStoreCopyValue(storeRef, (__bridge CFStringRef)globalPath);
    
    /* Expect global to contain something like
     ServiceOrder =     (
     "3E2652AC-CD50-4E4B-AA9B-872CB8E0E5D3",
     "CCC216BF-393A-41FE-87DB-A36E65C59A10",
     "041A0098-0BF4-4F0B-9C2D-39E034B807E6"
     );
     */
    //NSLog(@"serviceOrderList %@",global);
    NSArray *serviceOrderList = global[(__bridge NSString*)kSCPropNetServiceOrder];
    
    for (NSString *serviceID in serviceOrderList) {
        //NSLog(@"serviceID: %@", serviceID);
        // equivalent to CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("Setup:/Network/Service/%@/Interface"), serviceID);
        NSString* path = (__bridge_transfer NSString*)SCDynamicStoreKeyCreateNetworkServiceEntity(kCFAllocatorDefault, kSCDynamicStoreDomainSetup, (__bridge CFStringRef)serviceID, kSCEntNetInterface);
        NSDictionary *serviceInterface = (__bridge_transfer NSDictionary *)SCDynamicStoreCopyValue(storeRef, (__bridge CFStringRef)path);
        /* Expect serviceInterface to contain something like
         DeviceName = en0;
         Hardware = AirPort;
         Type = Ethernet;
         UserDefinedName = "Wi-Fi";
         */
        //NSLog(@"serviceInterface %@",serviceInterface);
        NSString *interfaceName = serviceInterface[(__bridge NSString*)kSCPropNetInterfaceDeviceName];
        NSString *hardware = serviceInterface[(__bridge NSString*)kSCPropNetInterfaceHardware];
        NSString *type = serviceInterface[(__bridge NSString*)kSCPropNetInterfaceType];
        //NSString *userDefinedName = serviceInterface[(__bridge NSString*)kSCPropUserDefinedName];
        if ([interfaceName hasPrefix:@"en"] &&
            [hardware isEqualToString:(__bridge NSString*)kSCValNetInterfaceTypeEthernet] &&
            [type isEqualToString:(__bridge NSString*)kSCValNetInterfaceTypeEthernet]) {
            
            SCNetworkInterfaceRef interface = _SCNetworkInterfaceCreateWithBSDName(kCFAllocatorDefault, (__bridge CFStringRef)(interfaceName), 0);
            BOOL isActive = NO;
            // *** RY: TODO: Figure out better way to restrict or handle more interfaces
            if ( interface ) {
                NSLog(@"Found and Ethernet interface %@",serviceInterface);
                if (!_SCNetworkInterfaceIsThunderbolt(interface) &&
                    !_SCNetworkInterfaceIsTethered(interface) &&
                    !_SCNetworkInterfaceIsBuiltin(interface)) {
            
                    NSLog(@"Found and Ethernet interface %@",serviceInterface);
                    // Equivalent to CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("State:/Network/Interface/%@/Link"), interfaceName);
                    NSString *linkPath = (__bridge_transfer NSString*)SCDynamicStoreKeyCreateNetworkInterfaceEntity(kCFAllocatorDefault, kSCDynamicStoreDomainState, (__bridge CFStringRef)interfaceName, kSCEntNetLink);
                    NSDictionary *linkDict = (__bridge_transfer NSDictionary*)SCDynamicStoreCopyValue(storeRef, (__bridge CFStringRef)linkPath);
                    isActive = [linkDict[(__bridge NSString*)kSCPropNetLinkActive] boolValue];
                } else {
                    NSLog(@"Sadly said interface was not suitable");
                }
                CFRelease(interface);
            }
            
            NSLog(@"Active = %@",isActive ? @"YES" : @"NO");
            if (isActive) {
                result = interfaceName;
                break;
            }
        }
    }
    return result;
}

+ (NSString *)ipv4AddressForInterface:(NSString *)interfaceName
{
    if (!interfaceName || ![interfaceName UTF8String]) return nil;
    
    /* determine UDN according to MAC address */
    int sock = socket (AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        NSLog(@"Error creating socket to return IP");
        return NULL;
    }
    
    struct ifreq ifr;
    strcpy(ifr.ifr_name, [interfaceName UTF8String]);
    ifr.ifr_addr.sa_family = AF_INET;
    
    if (ioctl (sock, SIOCGIFADDR, &ifr) < 0)
    {
        NSLog(@"Error calling ioctl to return IP");
        close (sock);
        return NULL;
    }
    
    uint32_t ip = ((struct sockaddr_in *) &ifr.ifr_addr)->sin_addr.s_addr;
    ip = ntohl (ip);
    NSString *result = [NSString stringWithFormat:@"%d.%d.%d.%d",(ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF];
    
    close (sock);
    
    return result;
    
}

#pragma mark Handshaking algorithm

// Makes sure we have an active ethernet interace with a link local IP address
- (BOOL)discoverLocalInterface
{
    //self.localInterfaceName = nil;
    self.localIP = nil;
    
    self.localInterfaceName = self.localInterfaceName ? self.localInterfaceName : [MCDeviceDiscovery localEthernetInterfaceName];
    if (!self.localInterfaceName) {
        NSLog(@"%s: couldn't find a local active ethernet interface", __PRETTY_FUNCTION__);
        return NO;
    }
    self.localIP = [MCDeviceDiscovery ipv4AddressForInterface:self.localInterfaceName];
    if (!self.localIP) {
        NSLog(@"%s: local active ethernet interface %@ doesn't have an IP", __PRETTY_FUNCTION__, self.localInterfaceName);
        return NO;
    }
    // We don't know the remote IP yet
    self.remoteIP = @"NONE";

    return YES;
}

// Initializes multicastAddr and sendSocketFd state
- (BOOL)setUpMulticastSendSocket
{
    NSLog(@"%s: setting up multicast send socket", __PRETTY_FUNCTION__);

    if (!self.localInterfaceName || !self.localIP) {
        NSLog(@"%s: must have a local ethernet interface and IP to initialize multicast sending", __PRETTY_FUNCTION__);
        return NO;
    }
    
    struct addrinfo hints;
    bzero(&hints, sizeof(hints));
    hints.ai_family = AF_INET6;
    hints.ai_flags = AI_NUMERICHOST;
    hints.ai_socktype = SOCK_DGRAM;
    
    int err_num;
    err_num = getaddrinfo(kHandshakingMulticastAddress, kHandshakingMulticastPort, &hints, &_multicastAddr);
    if(err_num) {
        NSLog(@"Error: %s calling getaddrinfo: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }

    _sendSocketFd = socket(_multicastAddr->ai_family, _multicastAddr->ai_socktype, _multicastAddr->ai_protocol);
    if (_sendSocketFd < 0) {
        NSLog(@"Error: %s creating socket: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }
    
    unsigned int ifindex;
    ifindex = if_nametoindex([self.localInterfaceName UTF8String]);
    if (setsockopt(_sendSocketFd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex,sizeof(ifindex)) !=0) {
        NSLog(@"Error: %s setsockopt/IPV6_MULTICAST_IF: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }
    
    return YES;
}

// initializes receiveSocketFd and joins multicast group for receiving
// must be called after setUpMulticastSendSocket because it uses multicastAddr
- (BOOL)setUpMulticastReceiveSocket
{
    NSLog(@"%s: setting up multicast receive socket", __PRETTY_FUNCTION__);
    
    if (!self.localInterfaceName || !self.localIP) {
        NSLog(@"%s: must have a local ethernet interface and IP to initialize multicast receiving", __PRETTY_FUNCTION__);
        return NO;
    }
    
    if (!_multicastAddr) {
        NSLog(@"%s: multicastAddr hasn't been initialized", __PRETTY_FUNCTION__);
        return NO;
    }

    // Get local address to intialize receiveSocketFd
    struct addrinfo hints;
    struct addrinfo *localAddr;
    bzero(&hints, sizeof(hints));
    hints.ai_family   = _multicastAddr->ai_family;
    hints.ai_socktype = _multicastAddr->ai_socktype;
    hints.ai_flags    = AI_PASSIVE;
    int err_num;
    err_num = getaddrinfo(NULL, kHandshakingMulticastPort, &hints, &localAddr);
    if(err_num) {
        NSLog(@"Error: %s calling getaddrinfo: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }
    
    _receiveSocketFd = socket(localAddr->ai_family, localAddr->ai_socktype, localAddr->ai_protocol);
    if (_receiveSocketFd < 0) {
        NSLog(@"Error: %s creating socket: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }
    
    if (bind(_receiveSocketFd, localAddr->ai_addr, localAddr->ai_addrlen) < 0) {
        NSLog(@"Error: %s binding socket: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }
    freeaddrinfo(localAddr);
    
    // set to non-blocking
    int flags;
    flags = fcntl(_receiveSocketFd,F_GETFL);
    if (fcntl(_receiveSocketFd, F_SETFL, flags | O_NONBLOCK) == -1){
        NSLog(@"Error: %s setting socket to non blocking: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }
    
    // Join the multicast group
    struct ipv6_mreq multicastRequest;
    const char *interfaceName = [self.localInterfaceName UTF8String];
    multicastRequest.ipv6mr_interface = if_nametoindex(interfaceName);
    multicastRequest.ipv6mr_multiaddr = ((struct sockaddr_in6*)(_multicastAddr->ai_addr))->sin6_addr;
    
    if ( setsockopt(_receiveSocketFd, IPPROTO_IPV6, IPV6_JOIN_GROUP, (char*) &multicastRequest, sizeof(multicastRequest)) != 0 )
    {
        NSLog(@"Error: %s joining multicast group: %s", __PRETTY_FUNCTION__, gai_strerror(err_num));
        return NO;
    }
    
    return YES;
}

- (BOOL)setUpReceiveDispatchSource
{
    NSLog(@"%s: setting up asynchronous reading via dispatch_source", __PRETTY_FUNCTION__);
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    self.readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, _receiveSocketFd, 0, queue);
    if (!self.readSource)
    {
        NSLog(@"Error: %s setting up dispatch source", __PRETTY_FUNCTION__);
        return NO;
    }
    
    // Install the event handler
    dispatch_source_set_event_handler(self.readSource, ^{
        if (dispatch_source_get_data(self.readSource) > 0) {
            [self receiveHandshakeMulticast];
        }
    });
    
    // Install the cancellation handler
    dispatch_source_set_cancel_handler(self.readSource, ^{
        NSLog(@"%s: canceling read source", __PRETTY_FUNCTION__);
    });
    
    // Start reading the file.
    dispatch_resume(self.readSource);
    
    return YES;
}

- (void)tearDownMulticastSendReceiveState
{
    if ( self.multicastSendTimer ) {
        dispatch_source_cancel(self.multicastSendTimer);
        self.multicastSendTimer = nil;
    }

    if ( _sendSocketFd ) {
        close (_sendSocketFd);
        _sendSocketFd = 0;
    }
    
    if ( _receiveSocketFd ) {
        close (_receiveSocketFd);
        _receiveSocketFd = 0;
    }
    
    if ( _multicastAddr ) {
        freeaddrinfo(_multicastAddr);
        _multicastAddr = nil;
    }
    
    if ( self.readSource ) {
        dispatch_source_cancel(self.readSource);
        self.readSource = nil;
    }
    
}

// Sends a multicast message string composed of the local IP plus the known remote IP
- (BOOL)sendHandshakeMulticast
{
    if (!_sendSocketFd) return NO;
    
    NSString *messageString = [NSString stringWithFormat:@"%@:%@:%@", kHandshakeMessagePrefix, self.localIP, self.remoteIP];
    
    //NSLog(@"%s: sending %@", __PRETTY_FUNCTION__,messageString);
    
    const char *msg = [messageString UTF8String];
    if (sendto(_sendSocketFd, msg, strlen(msg), 0, _multicastAddr->ai_addr, _multicastAddr->ai_addrlen) < 0) {
        NSLog(@"%s: sendto failed %s", __PRETTY_FUNCTION__,gai_strerror(errno));
        return NO;
    }
    return YES;
}

- (BOOL)receiveHandshakeMulticast
{
    const int kBufferSize = 256;
    char msgbuf[kBufferSize];
    bzero(&msgbuf, kBufferSize);
    struct sockaddr_in6 rcv_addr;
    int rcv_addr_len=sizeof(rcv_addr);
    
    if ((recvfrom(_receiveSocketFd, msgbuf, kBufferSize, 0,(struct sockaddr *) &rcv_addr,(socklen_t *)&rcv_addr_len)) < 0) {
        NSLog(@"%s: recvfrom failed %s", __PRETTY_FUNCTION__,gai_strerror(errno));
        return NO;
    }

    //NSLog(@"%s message=%s", __PRETTY_FUNCTION__, msgbuf);
    
    NSString *receivedString = [NSString stringWithFormat:@"%s",msgbuf];
    if (![receivedString hasPrefix:kHandshakeMessagePrefix]) {
        NSLog(@"%s: received message with unexpected prefix %@", __PRETTY_FUNCTION__,receivedString);
        return NO;
    }
    
    NSArray *receivedComponents = [receivedString componentsSeparatedByString:@":"];
    NSUInteger componentsCount = [receivedComponents count];
    if (componentsCount != 3) {
        NSLog(@"%s: expected message with 3 components and got %@", __PRETTY_FUNCTION__,receivedComponents);
        return NO;
    }
    NSString *messageLocalIP = receivedComponents[1];
    NSString *messageRemoteIP = receivedComponents[2];
    // only process messages we didn't send
    if (![self.localIP isEqualToString:messageLocalIP]) {
        // remember the message's sender IP as the remote IP
        self.remoteIP = messageLocalIP;
        if ([self.localIP isEqualToString:messageRemoteIP]) {
            // The message sent by another host contains our own IP as a remote IP
            // This means the sender knows about us and the handshake is done
            self.didCompleteHandshake = YES;
        }
    }
    return YES;
}

- (BOOL)setUpMulticastSendTimer
{
    self.multicastSendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.handshakeWorkQueue);
    if (self.multicastSendTimer)
    {
        dispatch_source_set_timer(self.multicastSendTimer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(self.multicastSendTimer, ^ {
            [self sendHandshakeMulticast];
            if (self.didCompleteHandshake) {
                if (self.handshakeCompletionBlock) {
                    self.handshakeCompletionBlock(YES,self.localInterfaceName,self.remoteIP);
                }
                [self tearDownMulticastSendReceiveState];
            };
        });
        dispatch_resume(self.multicastSendTimer);
    }

    return YES;
}

- (void)performHandshakeWithCompletionBlock:(MCDeviceHandshakeCompletion)completion
{
    if ( !self.handshakeWorkQueue )
        self.handshakeWorkQueue = dispatch_queue_create("com.apple.hid.handshakeworkqueue" , DISPATCH_QUEUE_SERIAL);

    self.handshakeCompletionBlock = completion;
    BOOL didStart = NO;
    
    if ([self discoverLocalInterface]) {
        if ([self setUpMulticastSendSocket]) {
            if ([self setUpMulticastReceiveSocket]) {
                if ([self setUpReceiveDispatchSource]) {
                    didStart = [self setUpMulticastSendTimer];
                }
            }
        }
    }
    if (!didStart) {
        [self tearDownMulticastSendReceiveState];
        if (self.handshakeCompletionBlock) {
            // TODO - do a much better job at propagating an error code when an error happens.
            self.handshakeCompletionBlock(NO,nil,nil);
        }
    }
}

@end