PortMapper/MYPortMapper.m
author Jens Alfke <jens@mooseyard.com>
Wed Apr 22 16:45:39 2009 -0700 (2009-04-22)
changeset 26 cb9cdf247239
child 27 92581f26073e
permissions -rw-r--r--
* Added MYBonjourBrowser and MYBonjourService.
* Added MYPortMapper.
* Added -[TCPEndpoint setPeerToPeerIdentity:].
* Created a static-library target.
     1 //
     2 //  MYPortMapper.m
     3 //  MYNetwork
     4 //
     5 //  Created by Jens Alfke on 1/4/08.
     6 //  Copyright 2008 Jens Alfke. All rights reserved.
     7 //
     8 
     9 #import "MYPortMapper.h"
    10 #import "IPAddress.h"
    11 #import "CollectionUtils.h"
    12 #import "Logging.h"
    13 #import "ExceptionUtils.h"
    14 
    15 #import <dns_sd.h>
    16 #import <sys/types.h>
    17 #import <sys/socket.h>
    18 #import <net/if.h>
    19 #import <netinet/in.h>
    20 #import <ifaddrs.h>
    21 
    22 
    23 NSString* const MYPortMapperChangedNotification = @"MYPortMapperChanged";
    24 
    25 
    26 @interface MYPortMapper ()
    27 // Redeclare these properties as settable, internally:
    28 @property (readwrite) SInt32 error;
    29 @property (retain) IPAddress* publicAddress, *localAddress;
    30 // Private getter:
    31 @property (readonly) void* _service;
    32 - (void) priv_updateLocalAddress;
    33 - (void) priv_disconnect;
    34 @end
    35 
    36 
    37 @implementation MYPortMapper
    38 
    39 
    40 - (id) initWithLocalPort: (UInt16)localPort
    41 {
    42     self = [super init];
    43     if (self != nil) {
    44         _localPort = localPort;
    45         _mapTCP = YES;
    46         [self priv_updateLocalAddress];
    47     }
    48     return self;
    49 }
    50 
    51 - (id) initWithNullMapping
    52 {
    53     // A PortMapper with no port or protocols will cause the DNSService to look up 
    54     // our public address without creating a mapping.
    55     if ([self initWithLocalPort: 0]) {
    56         _mapTCP = _mapUDP = NO;
    57     }
    58     return self;
    59 }
    60 
    61 
    62 - (void) dealloc
    63 {
    64     if( _service )
    65         [self priv_disconnect];
    66     [_publicAddress release];
    67     [_localAddress release];
    68     [super dealloc];
    69 }
    70 
    71 - (void) finalize
    72 {
    73     if( _service )
    74         [self priv_disconnect];
    75     [super finalize];
    76 }
    77 
    78 
    79 @synthesize localAddress=_localAddress, publicAddress=_publicAddress,
    80             error=_error, _service=_service,
    81             mapTCP=_mapTCP, mapUDP=_mapUDP,
    82             desiredPublicPort=_desiredPublicPort;
    83 
    84 
    85 - (BOOL) isMapped
    86 {
    87     return ! $equal(_publicAddress,_localAddress);
    88 }
    89 
    90 - (void) priv_updateLocalAddress 
    91 {
    92     IPAddress *localAddress = [IPAddress localAddressWithPort: _localPort];
    93     if (!$equal(localAddress,_localAddress))
    94         self.localAddress = localAddress;
    95 }
    96 
    97 
    98 static IPAddress* makeIPAddr( UInt32 rawAddr, UInt16 port ) {
    99     if (rawAddr)
   100         return [[[IPAddress alloc] initWithIPv4: rawAddr port: port] autorelease];
   101     else
   102         return nil;
   103 }
   104 
   105 /** Called whenever the port mapping changes (see comment for callback, below.) */
   106 - (void) priv_portMapStatus: (DNSServiceErrorType)errorCode 
   107               publicAddress: (UInt32)rawPublicAddress
   108                  publicPort: (UInt16)publicPort
   109 {
   110     LogTo(PortMapper,@"Callback got err %i, addr %08X:%hu",
   111           errorCode, rawPublicAddress, publicPort);
   112     if( errorCode==kDNSServiceErr_NoError ) {
   113         if( rawPublicAddress==0 || (publicPort==0 && (_mapTCP || _mapUDP)) ) {
   114             LogTo(PortMapper,@"(Callback reported no mapping available)");
   115             errorCode = kDNSServiceErr_NATPortMappingUnsupported;
   116         }
   117     }
   118     if( errorCode != self.error )
   119         self.error = errorCode;
   120 
   121     [self priv_updateLocalAddress];
   122     IPAddress *publicAddress = makeIPAddr(rawPublicAddress,publicPort);
   123     if (!$equal(publicAddress,_publicAddress))
   124         self.publicAddress = publicAddress;
   125     
   126     if( ! errorCode ) {
   127         LogTo(PortMapper,@"Callback got %08X:%hu -> %@ (mapped=%i)",
   128               rawPublicAddress,publicPort, self.publicAddress, self.isMapped);
   129     }
   130     [[NSNotificationCenter defaultCenter] postNotificationName: MYPortMapperChangedNotification
   131                                                         object: self];
   132 }
   133 
   134 
   135 /** Asynchronous callback from DNSServiceNATPortMappingCreate.
   136     This is invoked whenever the status of the port mapping changes.
   137     All it does is dispatch to the object's priv_portMapStatus:publicAddress:publicPort: method. */
   138 static void portMapCallback (
   139                       DNSServiceRef                    sdRef,
   140                       DNSServiceFlags                  flags,
   141                       uint32_t                         interfaceIndex,
   142                       DNSServiceErrorType              errorCode,
   143                       uint32_t                         publicAddress,    /* four byte IPv4 address in network byte order */
   144                       DNSServiceProtocol               protocol,
   145                       uint16_t                         privatePort,
   146                       uint16_t                         publicPort,       /* may be different than the requested port */
   147                       uint32_t                         ttl,              /* may be different than the requested ttl */
   148                       void                             *context
   149                       )
   150 {
   151     NSAutoreleasePool *pool = [NSAutoreleasePool new];
   152     @try{
   153         [(MYPortMapper*)context priv_portMapStatus: errorCode 
   154                                      publicAddress: publicAddress
   155                                         publicPort: ntohs(publicPort)];  // port #s in network byte order!
   156     }catchAndReport(@"PortMapper");
   157     [pool drain];
   158 }
   159 
   160 
   161 /** CFSocket callback, informing us that _socket has data available, which means
   162     that the DNS service has an incoming result to be processed. This will end up invoking
   163     the portMapCallback. */
   164 static void serviceCallback(CFSocketRef s, 
   165                             CFSocketCallBackType type,
   166                             CFDataRef address, const void *data, void *clientCallBackInfo)
   167 {
   168     MYPortMapper *mapper = (MYPortMapper*)clientCallBackInfo;
   169     DNSServiceRef service = mapper._service;
   170     DNSServiceErrorType err = DNSServiceProcessResult(service);
   171     if( err ) {
   172         // An error here means the socket has failed and should be closed.
   173         [mapper priv_portMapStatus: err publicAddress: 0 publicPort: 0];
   174         [mapper priv_disconnect];
   175     }
   176 }
   177 
   178 
   179 
   180 - (BOOL) open
   181 {
   182     NSAssert(!_service,@"Already open");
   183     // Create the DNSService:
   184     DNSServiceProtocol protocols = 0;
   185     if( _mapTCP ) protocols |= kDNSServiceProtocol_TCP;
   186     if( _mapUDP ) protocols |= kDNSServiceProtocol_UDP;
   187     self.error = DNSServiceNATPortMappingCreate((DNSServiceRef*)&_service, 
   188                                          0 /*flags*/, 
   189                                          0 /*interfaceIndex*/, 
   190                                          protocols,
   191                                          htons(_localPort),
   192                                          htons(_desiredPublicPort),
   193                                          0 /*ttl*/,
   194                                          &portMapCallback, 
   195                                          self);
   196     if( _error ) {
   197         LogTo(PortMapper,@"Error %i creating port mapping",_error);
   198         return NO;
   199     }
   200     
   201     // Wrap a CFSocket around the service's socket:
   202     CFSocketContext ctxt = { 0, self, CFRetain, CFRelease, NULL };
   203     _socket = CFSocketCreateWithNative(NULL, 
   204                                        DNSServiceRefSockFD(_service), 
   205                                        kCFSocketReadCallBack, 
   206                                        &serviceCallback, &ctxt);
   207     if( _socket ) {
   208         CFSocketSetSocketFlags(_socket, CFSocketGetSocketFlags(_socket) & ~kCFSocketCloseOnInvalidate);
   209         // Attach the socket to the runloop so the serviceCallback will be invoked:
   210         _socketSource = CFSocketCreateRunLoopSource(NULL, _socket, 0);
   211         if( _socketSource )
   212             CFRunLoopAddSource(CFRunLoopGetCurrent(), _socketSource, kCFRunLoopCommonModes);
   213     }
   214     if( _socketSource ) {
   215         LogTo(PortMapper,@"Opening");
   216         return YES;
   217     } else {
   218         Warn(@"Failed to open PortMapper");
   219         [self close];
   220         _error = kDNSServiceErr_Unknown;
   221         return NO;
   222     }
   223 }
   224 
   225 
   226 - (BOOL) waitTillOpened
   227 {
   228     if( ! _socketSource )
   229         if( ! [self open] )
   230             return NO;
   231     // Run the runloop until there's either an error or a result:
   232     while( _error==0 && _publicAddress==nil )
   233         if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
   234                                        beforeDate: [NSDate distantFuture]] )
   235             break;
   236     return (_error==0);
   237 }
   238 
   239 
   240 // Close down, but _without_ clearing the 'error' property
   241 - (void) priv_disconnect
   242 {
   243     if( _socketSource ) {
   244         CFRunLoopSourceInvalidate(_socketSource);
   245         CFRelease(_socketSource);
   246         _socketSource = NULL;
   247     }
   248     if( _socket ) {
   249         CFSocketInvalidate(_socket);
   250         CFRelease(_socket);
   251         _socket = NULL;
   252     }
   253     if( _service ) {
   254         LogTo(PortMapper,@"Deleting port mapping");
   255         DNSServiceRefDeallocate(_service);
   256         _service = NULL;
   257         self.publicAddress = nil;
   258     }
   259 }
   260 
   261 - (void) close
   262 {
   263     [self priv_disconnect];
   264     self.error = 0;
   265 }
   266 
   267 
   268 + (IPAddress*) findPublicAddress
   269 {
   270     IPAddress *addr = nil;
   271     MYPortMapper *mapper = [[self alloc] initWithNullMapping];
   272     if( [mapper waitTillOpened] )
   273         addr = [mapper.publicAddress retain];
   274     [mapper close];
   275     [mapper release];
   276     return [addr autorelease];
   277 }
   278 
   279 
   280 @end
   281 
   282 
   283 /*
   284  Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   285  
   286  Redistribution and use in source and binary forms, with or without modification, are permitted
   287  provided that the following conditions are met:
   288  
   289  * Redistributions of source code must retain the above copyright notice, this list of conditions
   290  and the following disclaimer.
   291  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   292  and the following disclaimer in the documentation and/or other materials provided with the
   293  distribution.
   294  
   295  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   296  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   297  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   298  BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   299  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   300   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   301  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   302  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   303  */