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