/****************************************************************************** * $Id$ * * Copyright (c) 2006 Transmission authors and contributors * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *****************************************************************************/ #include #include #include #include #include #include #include "transmission.h" #include "natpmp.h" #include "net.h" #include "platform.h" /* tr_getDefaultRoute() */ #include "utils.h" #define PMP_PORT 5351 #define PMP_MCAST_ADDR "224.0.0.1" #define PMP_INITIAL_DELAY 250 /* ms, 1/4 second */ #define PMP_TOTAL_DELAY 120000 /* ms, 2 minutes */ #define PMP_VERSION 0 #define PMP_OPCODE_GETIP 0 #define PMP_OPCODE_ADDUDP 1 #define PMP_OPCODE_ADDTCP 2 #define PMP_LIFETIME 3600 /* secs, one hour */ #define PMP_RESULT_OK 0 #define PMP_RESULT_BADVERS 1 #define PMP_RESULT_REFUSED 2 #define PMP_RESULT_NETDOWN 3 #define PMP_RESULT_NOMEM 4 #define PMP_RESULT_BADOPCODE 5 #define PMP_OPCODE_FROM_RESPONSE( op ) ( 0x80 ^ (op) ) #define PMP_OPCODE_TO_RESPONSE( op ) ( 0x80 | (op) ) #define PMP_OPCODE_IS_RESPONSE( op ) ( 0x80 & (op) ) #define PMP_TOBUF16( buf, num ) ( *( (uint16_t *) (buf) ) = htons( (num) ) ) #define PMP_TOBUF32( buf, num ) ( *( (uint32_t *) (buf) ) = htonl( (num) ) ) #define PMP_FROMBUF16( buf ) ( htons( *( (uint16_t *) (buf) ) ) ) #define PMP_FROMBUF32( buf ) ( htonl( *( (uint32_t *) (buf) ) ) ) typedef struct tr_natpmp_uptime_s { time_t when; uint32_t uptime; } tr_natpmp_uptime_t; typedef struct tr_natpmp_req_s { unsigned int adding : 1; unsigned int nobodyhome : 1; unsigned int tmpfail : 1; int fd; int delay; uint64_t retry; uint64_t timeout; int askport; int gotport; } tr_natpmp_req_t; struct tr_natpmp_s { #define PMP_STATE_IDLE 1 #define PMP_STATE_ADDING 2 #define PMP_STATE_DELETING 3 #define PMP_STATE_MAPPED 4 #define PMP_STATE_FAILED 5 #define PMP_STATE_NOBODYHOME 6 #define PMP_STATE_TMPFAIL 7 char state; unsigned int active : 1; unsigned int mapped : 1; struct in_addr dest; int newport; int mappedport; uint64_t renew; tr_natpmp_req_t * req; tr_natpmp_uptime_t uptime; int mcastfd; }; typedef struct tr_natpmp_parse_s { unsigned int tmpfail : 1; uint32_t seconds; uint16_t port; uint32_t lifetime; } tr_natpmp_parse_t; static void unmap( tr_natpmp_t * pmp ); static int checktime( tr_natpmp_uptime_t * uptime, uint32_t seen ); static void killsock( int * fd ); static tr_natpmp_req_t * newreq( int adding, struct in_addr addr, int port ); static void killreq( tr_natpmp_req_t ** req ); static void resetreq( tr_natpmp_req_t * req ); static tr_tristate_t pulsereq( tr_natpmp_t * req ); static int sendreq( tr_natpmp_req_t * req ); static int mcastsetup(); static void mcastpulse( tr_natpmp_t * pmp ); static tr_tristate_t parseresponse( uint8_t * buf, int len, int port, tr_natpmp_parse_t * parse ); tr_natpmp_t * tr_natpmpInit() { tr_natpmp_t * pmp; pmp = calloc( 1, sizeof( *pmp ) ); if( NULL == pmp ) { return NULL; } pmp->state = PMP_STATE_IDLE; pmp->mcastfd = -1; if( tr_getDefaultRoute( &pmp->dest ) || INADDR_ANY == pmp->dest.s_addr ) { pmp->dest.s_addr = INADDR_NONE; } if( INADDR_NONE == pmp->dest.s_addr ) { tr_dbg( "nat-pmp device is unknown" ); } else { char addrstr[INET_ADDRSTRLEN]; tr_netNtop( &pmp->dest, addrstr, sizeof( addrstr ) ); tr_dbg( "nat-pmp device is %s", addrstr ); } return pmp; } void tr_natpmpStart( tr_natpmp_t * pmp ) { if( !pmp->active ) { tr_inf( "starting nat-pmp" ); pmp->active = 1; if( 0 > pmp->mcastfd ) { pmp->mcastfd = mcastsetup(); } } } void tr_natpmpStop( tr_natpmp_t * pmp ) { if( pmp->active ) { tr_inf( "stopping nat-pmp" ); pmp->active = 0; killsock( &pmp->mcastfd ); unmap( pmp ); } } int tr_natpmpStatus( tr_natpmp_t * pmp ) { int ret; if( !pmp->active ) { ret = ( PMP_STATE_DELETING == pmp->state ? TR_NAT_TRAVERSAL_UNMAPPING : TR_NAT_TRAVERSAL_DISABLED ); } else if( pmp->mapped ) { ret = TR_NAT_TRAVERSAL_MAPPED; } else { switch( pmp->state ) { case PMP_STATE_IDLE: case PMP_STATE_ADDING: case PMP_STATE_DELETING: ret = TR_NAT_TRAVERSAL_MAPPING; break; case PMP_STATE_FAILED: case PMP_STATE_TMPFAIL: ret = TR_NAT_TRAVERSAL_ERROR; break; case PMP_STATE_NOBODYHOME: ret = TR_NAT_TRAVERSAL_NOTFOUND; break; case PMP_STATE_MAPPED: default: /* if pmp->state is PMP_STATE_MAPPED then pmp->mapped should be true */ assert( 0 ); ret = TR_NAT_TRAVERSAL_ERROR; break; } } return ret; } void tr_natpmpForwardPort( tr_natpmp_t * pmp, int port ) { tr_inf( "nat-pmp set port %i", port ); pmp->newport = port; } void tr_natpmpRemoveForwarding( tr_natpmp_t * pmp ) { tr_inf( "nat-pmp unset port" ); pmp->newport = -1; unmap( pmp ); } void tr_natpmpClose( tr_natpmp_t * pmp ) { /* try to send at least one delete request if we have a port mapping */ tr_natpmpStop( pmp ); tr_natpmpPulse( pmp, NULL ); killreq( &pmp->req ); free( pmp ); } void tr_natpmpPulse( tr_natpmp_t * pmp, int * publicPort ) { if( 0 <= pmp->mcastfd ) { mcastpulse( pmp ); } if( NULL != publicPort ) { *publicPort = -1; } if( pmp->active || PMP_STATE_DELETING == pmp->state ) { switch( pmp->state ) { case PMP_STATE_IDLE: case PMP_STATE_TMPFAIL: if( 0 < pmp->newport ) { tr_dbg( "nat-pmp state %s -> add with port %i", ( PMP_STATE_IDLE == pmp->state ? "idle" : "err" ), pmp->newport ); pmp->state = PMP_STATE_ADDING; } break; case PMP_STATE_ADDING: if( NULL == pmp->req ) { if( 0 >= pmp->newport ) { tr_dbg( "nat-pmp state add -> idle, no port" ); pmp->state = PMP_STATE_IDLE; } else if( INADDR_NONE == pmp->dest.s_addr ) { tr_dbg( "nat-pmp state add -> fail, no default route" ); pmp->state = PMP_STATE_FAILED; } else { pmp->req = newreq( 1, pmp->dest, pmp->newport ); if( NULL == pmp->req ) { pmp->state = PMP_STATE_FAILED; tr_dbg( "nat-pmp state add -> fail on req init" ); } } } if( PMP_STATE_ADDING == pmp->state ) { switch( pulsereq( pmp ) ) { case TR_NET_ERROR: if( pmp->req->nobodyhome ) { pmp->state = PMP_STATE_NOBODYHOME; tr_dbg( "nat-pmp state add -> nobodyhome on pulse" ); } else if( pmp->req->tmpfail ) { pmp->state = PMP_STATE_TMPFAIL; tr_dbg( "nat-pmp state add -> err on pulse" ); if( pmp->req->askport == pmp->newport ) { pmp->newport = 0; } } else { pmp->state = PMP_STATE_FAILED; tr_dbg( "nat-pmp state add -> fail on pulse" ); } killreq( &pmp->req ); break; case TR_NET_OK: pmp->mappedport = pmp->req->gotport; if( pmp->mappedport != pmp->newport && pmp->newport == pmp->req->askport ) { pmp->newport = pmp->req->gotport; } killreq( &pmp->req ); pmp->state = PMP_STATE_MAPPED; pmp->mapped = 1; tr_dbg( "nat-pmp state add -> mapped with port %i", pmp->mappedport); tr_inf( "nat-pmp mapped port %i", pmp->mappedport ); if( NULL != publicPort ) { *publicPort = pmp->mappedport; } break; case TR_NET_WAIT: break; } } break; case PMP_STATE_DELETING: if( NULL == pmp->req ) { assert( 0 < pmp->mappedport ); pmp->req = newreq( 0, pmp->dest, pmp->mappedport ); if( NULL == pmp->req ) { pmp->state = PMP_STATE_FAILED; tr_dbg( "nat-pmp state del -> fail on req init" ); } } if( PMP_STATE_DELETING == pmp->state ) { switch( pulsereq( pmp ) ) { case TR_NET_ERROR: if( pmp->req->nobodyhome ) { pmp->mapped = 0; pmp->state = PMP_STATE_NOBODYHOME; tr_dbg( "nat-pmp state del -> nobodyhome on pulse" ); } else if( pmp->req->tmpfail ) { pmp->mapped = 0; pmp->state = PMP_STATE_TMPFAIL; tr_dbg( "nat-pmp state del -> err on pulse" ); pmp->mappedport = -1; } else { pmp->state = PMP_STATE_FAILED; tr_dbg( "nat-pmp state del -> fail on pulse" ); } killreq( &pmp->req ); break; case TR_NET_OK: tr_dbg( "nat-pmp state del -> idle with port %i", pmp->req->askport); tr_inf( "nat-pmp unmapped port %i", pmp->req->askport ); pmp->mapped = 0; pmp->mappedport = -1; killreq( &pmp->req ); pmp->state = PMP_STATE_IDLE; break; case TR_NET_WAIT: break; } } break; case PMP_STATE_MAPPED: if( pmp->newport != pmp->mappedport ) { tr_dbg( "nat-pmp state mapped -> del, port from %i to %i", pmp->mappedport, pmp->newport ); pmp->state = PMP_STATE_DELETING; } else if( tr_date() > pmp->renew ) { pmp->state = PMP_STATE_ADDING; tr_dbg( "nat-pmp state mapped -> add for renewal" ); } break; case PMP_STATE_FAILED: case PMP_STATE_NOBODYHOME: break; default: assert( 0 ); break; } } } void unmap( tr_natpmp_t * pmp ) { switch( pmp->state ) { case PMP_STATE_IDLE: break; case PMP_STATE_ADDING: if( NULL == pmp->req ) { pmp->state = PMP_STATE_IDLE; tr_dbg( "nat-pmp state add -> idle" ); } else { pmp->mappedport = pmp->req->gotport; killreq( &pmp->req ); pmp->state = PMP_STATE_DELETING; tr_dbg( "nat-pmp state add -> del" ); } break; case PMP_STATE_DELETING: break; case PMP_STATE_MAPPED: pmp->state = PMP_STATE_DELETING; tr_dbg( "nat-pmp state mapped -> del" ); break; case PMP_STATE_FAILED: case PMP_STATE_NOBODYHOME: case PMP_STATE_TMPFAIL: break; default: assert( 0 ); break; } } static int checktime( tr_natpmp_uptime_t * uptime, uint32_t cursecs ) { time_t now; int ret; uint32_t estimated; now = time( NULL ); ret = 0; if( 0 < uptime->when ) { estimated = ( ( now - uptime->when ) * 7 / 8 ) + uptime->uptime; if( estimated > cursecs ) { ret = 1; } } uptime->when = now; uptime->uptime = cursecs; return ret; } static void killsock( int * fd ) { if( 0 <= *fd ) { tr_netClose( *fd ); *fd = -1; } } static tr_natpmp_req_t * newreq( int adding, struct in_addr addr, int port ) { tr_natpmp_req_t * ret; ret = calloc( 1, sizeof( *ret ) ); if( NULL == ret ) { return NULL; } ret->fd = tr_netOpenUDP( &addr, htons( PMP_PORT ), 1 ); if( 0 > ret->fd ) { free( ret ); return NULL; } ret->adding = adding; ret->askport = port; ret->gotport = port; resetreq( ret ); if( sendreq( ret ) ) { killreq( &ret ); return NULL; } return ret; } static void killreq( tr_natpmp_req_t ** req ) { if( NULL != *req ) { killsock( &(*req)->fd ); free( *req ); *req = NULL; } } static void resetreq( tr_natpmp_req_t * req ) { uint64_t now; now = tr_date(); req->delay = PMP_INITIAL_DELAY; req->retry = now; req->timeout = now + PMP_TOTAL_DELAY; } static tr_tristate_t pulsereq( tr_natpmp_t * pmp ) { tr_natpmp_req_t * req = pmp->req; struct sockaddr_in sin; uint8_t buf[16]; int res; uint64_t now; tr_tristate_t ret; tr_natpmp_parse_t parse; now = tr_date(); /* check for timeout */ if( now >= req->timeout ) { tr_dbg( "nat-pmp request timed out" ); req->nobodyhome = 1; return TR_NET_ERROR; } /* send another request if it's been long enough */ if( now >= req->retry && sendreq( req ) ) { return TR_NET_ERROR; } /* check for incoming packets */ res = tr_netRecvFrom( req->fd, buf, sizeof( buf ), &sin ); if( TR_NET_BLOCK & res ) { return TR_NET_WAIT; } else if( TR_NET_CLOSE & res ) { if( ECONNRESET == sockerrno || ECONNREFUSED == sockerrno ) { tr_dbg( "nat-pmp not supported by device" ); req->nobodyhome = 1; } else { tr_inf( "error reading nat-pmp response (%s)", strerror( sockerrno ) ); } return TR_NET_ERROR; } /* parse the packet */ tr_dbg( "nat-pmp read %i byte response", res ); ret = parseresponse( buf, res, req->askport, &parse ); req->tmpfail = parse.tmpfail; /* check for device reset */ if( checktime( &pmp->uptime, parse.seconds ) ) { pmp->renew = 0; tr_inf( "detected nat-pmp device reset" ); resetreq( req ); ret = TR_NET_WAIT; } if( TR_NET_OK == ret && req->adding ) { if( req->askport != parse.port ) { tr_dbg( "nat-pmp received %i for public port instead of %i", parse.port, req->askport ); req->gotport = parse.port; } tr_dbg( "nat-pmp set renew to half of %u", parse.lifetime ); pmp->renew = now + ( parse.lifetime / 2 * 1000 ); } return ret; } static int sendreq( tr_natpmp_req_t * req ) { uint8_t buf[12]; int res; memset( buf, 0, sizeof( buf ) ); buf[0] = PMP_VERSION; buf[1] = PMP_OPCODE_ADDTCP; PMP_TOBUF16( buf + 4, req->askport ); if( req->adding ) { PMP_TOBUF16( buf + 6, req->askport ); PMP_TOBUF32( buf + 8, PMP_LIFETIME ); } res = tr_netSend( req->fd, buf, sizeof( buf ) ); if( TR_NET_CLOSE & res && EHOSTUNREACH == sockerrno ) { res = TR_NET_BLOCK; } if( TR_NET_CLOSE & res ) { tr_err( "failed to send nat-pmp request (%s)", strerror( sockerrno ) ); return 1; } else if( !( TR_NET_BLOCK & res ) ) { /* XXX is it all right to assume the entire thing is written? */ req->retry = tr_date() + req->delay; req->delay *= 2; } return 0; } static int mcastsetup() { int fd; struct in_addr addr; addr.s_addr = inet_addr( PMP_MCAST_ADDR ); fd = tr_netMcastOpen( PMP_PORT, &addr ); if( 0 > fd ) { return -1; } tr_dbg( "nat-pmp create multicast socket %i", fd ); return fd; } static void mcastpulse( tr_natpmp_t * pmp ) { struct sockaddr_in sin; uint8_t buf[16]; int res; char dbgstr[INET_ADDRSTRLEN]; tr_natpmp_parse_t parse; res = tr_netRecvFrom( pmp->mcastfd, buf, sizeof( buf ), &sin ); if( TR_NET_BLOCK & res ) { return; } else if( TR_NET_CLOSE & res ) { tr_err( "error reading nat-pmp multicast message" ); killsock( &pmp->mcastfd ); return; } tr_netNtop( &sin.sin_addr, dbgstr, sizeof( dbgstr ) ); tr_dbg( "nat-pmp read %i byte multicast packet from %s", res, dbgstr ); if( pmp->dest.s_addr != sin.sin_addr.s_addr ) { tr_dbg( "nat-pmp ignoring multicast packet from unknown host %s", dbgstr ); return; } if( TR_NET_OK == parseresponse( buf, res, -1, &parse ) ) { if( checktime( &pmp->uptime, parse.seconds ) ) { pmp->renew = 0; tr_inf( "detected nat-pmp device reset" ); if( NULL != pmp->req ) { resetreq( pmp->req ); } } if( PMP_STATE_NOBODYHOME == pmp->state ) { tr_dbg( "nat-pmp state notfound -> idle" ); pmp->state = PMP_STATE_IDLE; } } } static tr_tristate_t parseresponse( uint8_t * buf, int len, int port, tr_natpmp_parse_t * parse ) { int version, respopcode, opcode, wantedopcode, rescode, privport; memset( parse, 0, sizeof( *parse ) ); if( 8 > len ) { tr_err( "read truncated %i byte nat-pmp response packet", len ); return TR_NET_ERROR; } /* parse the first 8 bytes: version, opcode, and result code */ version = buf[0]; respopcode = buf[1]; opcode = PMP_OPCODE_FROM_RESPONSE( respopcode ); wantedopcode = ( 0 < port ? PMP_OPCODE_ADDTCP : PMP_OPCODE_GETIP ); rescode = PMP_FROMBUF16( buf + 2 ); if( PMP_VERSION != version ) { tr_err( "unknown nat-pmp version %hhu", buf[0] ); return TR_NET_ERROR; } if( !PMP_OPCODE_IS_RESPONSE( respopcode ) ) { tr_dbg( "nat-pmp ignoring request packet" ); return TR_NET_WAIT; } if( wantedopcode != opcode ) { tr_err( "unknown nat-pmp opcode %hhu", opcode ); return TR_NET_ERROR; } switch( rescode ) { case PMP_RESULT_OK: break; case PMP_RESULT_REFUSED: tr_err( "nat-pmp mapping failed: refused/unauthorized/disabled" ); parse->tmpfail = 1; return TR_NET_ERROR; case PMP_RESULT_NETDOWN: tr_err( "nat-pmp mapping failed: network down" ); parse->tmpfail = 1; return TR_NET_ERROR; case PMP_RESULT_NOMEM: tr_err( "nat-pmp mapping refused: insufficient resources" ); parse->tmpfail = 1; return TR_NET_ERROR; default: tr_err( "nat-pmp mapping refused: unknown result code: %hu", rescode ); return TR_NET_ERROR; } parse->seconds = PMP_FROMBUF32( buf + 4 ); if( PMP_OPCODE_ADDTCP == opcode ) { if( 16 > len ) { tr_err( "read truncated %i byte nat-pmp response packet", len ); return TR_NET_ERROR; } privport = PMP_FROMBUF16( buf + 8 ); parse->port = PMP_FROMBUF16( buf + 10 ); parse->lifetime = PMP_FROMBUF32( buf + 12 ); if( port != privport ) { tr_dbg( "nat-pmp ignoring message for port %i, expected port %i", privport, port ); return TR_NET_WAIT; } } return TR_NET_OK; }