class Connection(object): direction = 'undefined' identifier = {} def __init__(self, afi, peer, local): self.msg_size = ExtendedMessage.INITIAL_SIZE # peer and local are strings of the IP try: self.defensive = environment.settings().debug.defensive self.logger = Logger() except RuntimeError: self.defensive = True self.logger = FakeLogger() self.afi = afi self.peer = peer self.local = local self.io = None self.established = False self._rpoller = {} self._wpoller = {} self.id = self.identifier.get(self.direction, 1) def success(self): identifier = self.identifier.get(self.direction, 1) + 1 self.identifier[self.direction] = identifier return identifier # Just in case .. def __del__(self): if self.io: self.close() self.logger.warning('connection to %s closed' % self.peer, self.session()) def name(self): return "%s-%d %s-%s" % (self.direction, self.id, self.local, self.peer) def session(self): return "%s-%d" % (self.direction, self.id) def fd(self): if self.io: return self.io.fileno() # the socket is closed (fileno() == -1) or not open yet (io is None) return -1 def close(self): try: self.logger.warning('%s, closing connection' % self.name(), source=self.session()) if self.io: self.io.close() self.io = None except KeyboardInterrupt as exc: raise exc except Exception: self.io = None def reading(self): poller = self._rpoller.get(self.io, None) if poller is None: poller = select.poll() poller.register( self.io, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLNVAL | select.POLLERR) self._rpoller = {self.io: poller} ready = False for _, event in poller.poll(0): if event & select.POLLIN or event & select.POLLPRI: ready = True elif event & select.POLLHUP or event & select.POLLERR or event & select.POLLNVAL: self._rpoller = {} ready = True return ready def writing(self): poller = self._wpoller.get(self.io, None) if poller is None: poller = select.poll() poller.register( self.io, select.POLLOUT | select.POLLHUP | select.POLLNVAL | select.POLLERR) self._wpoller = {self.io: poller} ready = False for _, event in poller.poll(0): if event & select.POLLOUT: ready = True elif event & select.POLLHUP or event & select.POLLERR or event & select.POLLNVAL: self._wpoller = {} ready = True return ready def _reader(self, number): # The function must not be called if it does not return with no data with a smaller size as parameter if not self.io: self.close() raise NotConnected('Trying to read on a closed TCP connection') if number == 0: yield b'' return while not self.reading(): yield b'' data = b'' reported = '' while True: try: while True: if self.defensive and random.randint(0, 2): raise socket.error(errno.EAGAIN, 'raising network error on purpose') read = self.io.recv(number) if not read: self.close() self.logger.warning( '%s %s lost TCP session with peer' % (self.name(), self.peer), self.session()) raise LostConnection( 'the TCP connection was closed by the remote end') data += read number -= len(read) if not number: self.logger.debug( LazyFormat('received TCP payload', data), self.session()) yield data return yield b'' except socket.timeout as exc: self.close() self.logger.warning( '%s %s peer is too slow' % (self.name(), self.peer), self.session()) raise TooSlowError( 'Timeout while reading data from the network (%s)' % errstr(exc)) except socket.error as exc: if exc.args[0] in error.block: message = '%s %s blocking io problem mid-way through reading a message %s, trying to complete' % ( self.name(), self.peer, errstr(exc)) if message != reported: reported = message self.logger.debug(message, self.session()) yield b'' elif exc.args[0] in error.fatal: self.close() raise LostConnection('issue reading on the socket: %s' % errstr(exc)) # what error could it be ! else: self.logger.critical( '%s %s undefined error reading on socket' % (self.name(), self.peer), self.session()) raise NetworkError( 'Problem while reading data from the network (%s)' % errstr(exc)) def writer(self, data): if not self.io: # XXX: FIXME: Make sure it does not hold the cleanup during the closing of the peering session yield True return while not self.writing(): yield False self.logger.debug(LazyFormat('sending TCP payload', data), self.session()) # The first while is here to setup the try/catch block once as it is very expensive while True: try: while True: if self.defensive and random.randint(0, 2): raise socket.error(errno.EAGAIN, 'raising network error on purpose') # we can not use sendall as in case of network buffer filling # it does raise and does not let you know how much was sent number = self.io.send(data) if not number: self.close() self.logger.warning( '%s %s lost TCP connection with peer' % (self.name(), self.peer), self.session()) raise LostConnection('lost the TCP connection') data = data[number:] if not data: yield True return yield False except socket.error as exc: if exc.args[0] in error.block: self.logger.debug( '%s %s blocking io problem mid-way through writing a message %s, trying to complete' % (self.name(), self.peer, errstr(exc)), self.session()) yield False elif exc.errno == errno.EPIPE: # The TCP connection is gone. self.close() raise NetworkError('Broken TCP connection') elif exc.args[0] in error.fatal: self.close() self.logger.critical( '%s %s problem sending message (%s)' % (self.name(), self.peer, errstr(exc)), self.session()) raise NetworkError( 'Problem while writing data to the network (%s)' % errstr(exc)) # what error could it be ! else: self.logger.critical( '%s %s undefined error writing on socket' % (self.name(), self.peer), self.session()) yield False def reader(self): # _reader returns the whole number requested or nothing and then stops for header in self._reader(Message.HEADER_LEN): if not header: yield 0, 0, b'', b'', None if not header.startswith(Message.MARKER): report = 'The packet received does not contain a BGP marker' yield 0, 0, header, b'', NotifyError(1, 1, report) return msg = ordinal(header[18]) length = unpack('!H', header[16:18])[0] if length < Message.HEADER_LEN or length > self.msg_size: report = '%s has an invalid message length of %d' % ( Message.CODE.name(msg), length) yield length, 0, header, b'', NotifyError(1, 2, report) return validator = Message.Length.get(msg, lambda _: _ >= 19) if not validator(length): # MUST send the faulty length back report = '%s has an invalid message length of %d' % ( Message.CODE.name(msg), length) yield length, 0, header, b'', NotifyError(1, 2, report) return number = length - Message.HEADER_LEN if not number: yield length, msg, header, b'', None return for body in self._reader(number): if not body: yield 0, 0, b'', b'', None yield length, msg, header, body, None
class Protocol (object): decode = True def __init__ (self, peer): try: self.logger = Logger() except RuntimeError: self.logger = FakeLogger() self.peer = peer self.neighbor = peer.neighbor self.negotiated = Negotiated(self.neighbor) self.connection = None if self.neighbor.connect: self.port = self.neighbor.connect elif os.environ.get('exabgp.tcp.port','').isdigit(): self.port = int(os.environ.get('exabgp.tcp.port')) elif os.environ.get('exabgp_tcp_port','').isdigit(): self.port = int(os.environ.get('exabgp_tcp_port')) else: self.port = 179 # XXX: FIXME: check the the -19 is correct (but it is harmless) # The message size is the whole BGP message _without_ headers self.message_size = Message.MAX_LEN-Message.HEADER_LEN from exabgp.configuration.environment import environment self.log_routes = peer.neighbor.adj_rib_in or environment.settings().log.routes # XXX: we use self.peer.neighbor.peer_address when we could use self.neighbor.peer_address def me (self, message): return "%s/%s %s" % (self.peer.neighbor.peer_address,self.peer.neighbor.peer_as,message) def accept (self, incoming): self.connection = incoming if self.peer.neighbor.api['neighbor-changes']: self.peer.reactor.processes.connected(self.peer.neighbor) # very important - as we use this function on __init__ return self def connect (self): # allows to test the protocol code using modified StringIO with a extra 'pending' function if not self.connection: local = self.neighbor.md5_ip.top() if not self.neighbor.auto_discovery else None peer = self.neighbor.peer_address.top() afi = self.neighbor.peer_address.afi md5 = self.neighbor.md5_password md5_base64 = self.neighbor.md5_base64 ttl_out = self.neighbor.ttl_out self.connection = Outgoing(afi,peer,local,self.port,md5,md5_base64,ttl_out) if not local and self.connection.init: self.neighbor.local_address = IP.create(self.connection.local) if self.neighbor.router_id is None and self.neighbor.local_address.afi == AFI.ipv4: self.neighbor.router_id = self.neighbor.local_address try: generator = self.connection.establish() while True: connected = six.next(generator) if not connected: yield False continue if self.peer.neighbor.api['neighbor-changes']: self.peer.reactor.processes.connected(self.peer.neighbor) yield True return except StopIteration: # close called by the caller # self.close('could not connect to remote end') yield False return def close (self, reason='protocol closed, reason unspecified'): if self.connection: self.logger.debug(reason,self.connection.session()) # must be first otherwise we could have a loop caused by the raise in the below self.connection.close() self.connection = None self.peer.stats['down'] = self.peer.stats.get('down',0) + 1 try: if self.peer.neighbor.api['neighbor-changes']: self.peer.reactor.processes.down(self.peer.neighbor,reason) except ProcessError: self.logger.debug('could not send notification of neighbor close to API',self.connection.session()) def _to_api (self,direction,message,raw): packets = self.neighbor.api['%s-packets' % direction] parsed = self.neighbor.api['%s-parsed' % direction] consolidate = self.neighbor.api['%s-consolidate' % direction] negotiated = self.negotiated if self.neighbor.api['negotiated'] else None if consolidate: if packets: self.peer.reactor.processes.message(self.peer.neighbor,direction,message,negotiated,raw[:19],raw[19:]) else: self.peer.reactor.processes.message(self.peer.neighbor,direction,message,negotiated,b'',b'') else: if packets: self.peer.reactor.processes.packets(self.peer.neighbor,direction,int(message.ID),negotiated,raw[:19],raw[19:]) if parsed: self.peer.reactor.processes.message(message.ID,self.peer.neighbor,direction,message,negotiated,b'',b'') def write (self, message, negotiated=None): raw = message.message(negotiated) code = 'send-%s' % Message.CODE.short(message.ID) self.peer.stats[code] = self.peer.stats.get(code,0) + 1 if self.neighbor.api.get(code,False): self._to_api('send',message,raw) for boolean in self.connection.writer(raw): yield boolean def send (self, raw): code = 'send-%s' % Message.CODE.short(ordinal(raw[18])) self.peer.stats[code] = self.peer.stats.get(code,0) + 1 if self.neighbor.api.get(code,False): message = Update.unpack_message(raw[19:],self.negotiated) self._to_api('send',message,raw) for boolean in self.connection.writer(raw): yield boolean # Read from network ....................................................... def read_message (self): # This will always be defined by the loop but scope leaking upset scrutinizer/pylint msg_id = None packets = self.neighbor.api['receive-packets'] consolidate = self.neighbor.api['receive-consolidate'] parsed = self.neighbor.api['receive-parsed'] body,header = b'',b'' # just because pylint/pylama are getting more clever for length,msg_id,header,body,notify in self.connection.reader(): # internal issue if notify: code = 'receive-%s' % Message.CODE.NOTIFICATION.SHORT if self.neighbor.api.get(code,False): if consolidate: self.peer.reactor.processes.notification(self.peer.neighbor,'receive',notify.code,notify.subcode,str(notify),None,header,body) elif parsed: self.peer.reactor.processes.notification(self.peer.neighbor,'receive',notify.code,notify.subcode,str(notify),None,b'',b'') elif packets: self.peer.reactor.processes.packets(self.peer.neighbor,'receive',msg_id,None,header,body) # XXX: is notify not already Notify class ? raise Notify(notify.code,notify.subcode,str(notify)) if not length: yield _NOP continue self.logger.debug('<< message of type %s' % Message.CODE.name(msg_id),self.connection.session()) code = 'receive-%s' % Message.CODE.short(msg_id) self.peer.stats[code] = self.peer.stats.get(code,0) + 1 for_api = self.neighbor.api.get(code,False) if for_api and packets and not consolidate: negotiated = self.negotiated if self.neighbor.api.get('negotiated',False) else None self.peer.reactor.processes.packets(self.peer.neighbor,'receive',msg_id,negotiated,header,body) if msg_id == Message.CODE.UPDATE: if not self.neighbor.adj_rib_in and not (for_api or self.log_routes) and not (parsed or consolidate): yield _UPDATE return try: message = Message.unpack(msg_id,body,self.negotiated) except (KeyboardInterrupt,SystemExit,Notify): raise except Exception as exc: self.logger.debug('could not decode message "%d"' % msg_id,self.connection.session()) self.logger.debug('%s' % str(exc),self.connection.session()) self.logger.debug(traceback.format_exc(),self.connection.session()) raise Notify(1,0,'can not decode update message of type "%d"' % msg_id) # raise Notify(5,0,'unknown message received') if message.TYPE == Update.TYPE: if Attribute.CODE.INTERNAL_TREAT_AS_WITHDRAW in message.attributes: for nlri in message.nlris: nlri.action = IN.WITHDRAWN if for_api: negotiated = self.negotiated if self.neighbor.api.get('negotiated',False) else None if consolidate: self.peer.reactor.processes.message(msg_id,self.neighbor,'receive',message,negotiated,header,body) elif parsed: self.peer.reactor.processes.message(msg_id,self.neighbor,'receive',message,negotiated,b'',b'') if message.TYPE == Notification.TYPE: raise message if message.TYPE == Update.TYPE and Attribute.CODE.INTERNAL_DISCARD in message.attributes: yield _NOP else: yield message def validate_open (self): error = self.negotiated.validate(self.neighbor) if error is not None: raise Notify(*error) if self.neighbor.api['negotiated']: self.peer.reactor.processes.negotiated(self.peer.neighbor,self.negotiated) if self.negotiated.mismatch: self.logger.warning('--------------------------------------------------------------------',self.connection.session()) self.logger.warning('the connection can not carry the following family/families',self.connection.session()) for reason,(afi,safi) in self.negotiated.mismatch: self.logger.warning(' - %s is not configured for %s/%s' % (reason,afi,safi),self.connection.session()) self.logger.warning('therefore no routes of this kind can be announced on the connection',self.connection.session()) self.logger.warning('--------------------------------------------------------------------',self.connection.session()) def read_open (self, ip): for received_open in self.read_message(): if received_open.TYPE == NOP.TYPE: yield received_open else: break if received_open.TYPE != Open.TYPE: raise Notify(5,1,'The first packet received is not an open message (%s)' % received_open) self.logger.debug('<< %s' % received_open,self.connection.session()) yield received_open def read_keepalive (self): for message in self.read_message(): if message.TYPE == NOP.TYPE: yield message else: break if message.TYPE != KeepAlive.TYPE: raise Notify(5,2) yield message # # Sending message to peer # def new_open (self): if self.neighbor.local_as: local_as = self.neighbor.local_as elif self.negotiated.received_open: local_as = self.negotiated.received_open.asn else: raise RuntimeError('no ASN available for the OPEN message') sent_open = Open( Version(4), local_as, self.neighbor.hold_time, self.neighbor.router_id, Capabilities().new(self.neighbor,self.peer._restarted) ) # we do not buffer open message in purpose for _ in self.write(sent_open): yield _NOP self.logger.debug('>> %s' % sent_open,self.connection.session()) yield sent_open def new_keepalive (self, comment=''): keepalive = KeepAlive() for _ in self.write(keepalive): yield _NOP self.logger.debug('>> KEEPALIVE%s' % (' (%s)' % comment if comment else ''),self.connection.session()) yield keepalive def new_notification (self, notification): for _ in self.write(notification): yield _NOP self.logger.debug('>> NOTIFICATION (%d,%d,"%s")' % (notification.code,notification.subcode,notification.data),self.connection.session()) yield notification def new_update (self, include_withdraw): updates = self.neighbor.rib.outgoing.updates(self.neighbor.group_updates) number = 0 for update in updates: for message in update.messages(self.negotiated,include_withdraw): number += 1 for boolean in self.send(message): # boolean is a transient network error we already announced yield _NOP if number: self.logger.debug('>> %d UPDATE(s)' % number,self.connection.session()) yield _UPDATE def new_eor (self, afi, safi): eor = EOR(afi,safi) for _ in self.write(eor): yield _NOP self.logger.debug('>> EOR %s %s' % (afi,safi),self.connection.session()) yield eor def new_eors (self, afi=AFI.undefined,safi=SAFI.undefined): # Send EOR to let our peer know he can perform a RIB update if self.negotiated.families: families = self.negotiated.families if (afi,safi) == (AFI.undefined,SAFI.undefined) else [(afi,safi),] for eor_afi,eor_safi in families: for _ in self.new_eor(eor_afi,eor_safi): yield _ else: # If we are not sending an EOR, send a keepalive as soon as when finished # So the other routers knows that we have no (more) routes to send ... # (is that behaviour documented somewhere ??) for eor in self.new_keepalive('EOR'): yield _NOP yield _UPDATE def new_operational (self, operational, negotiated): for _ in self.write(operational,negotiated): yield _NOP self.logger.debug('>> OPERATIONAL %s' % str(operational),self.connection.session()) yield operational def new_refresh (self, refresh): for _ in self.write(refresh,None): yield _NOP self.logger.debug('>> REFRESH %s' % str(refresh),self.connection.session()) yield refresh
class Protocol(object): decode = True def __init__(self, peer): try: self.logger = Logger() except RuntimeError: self.logger = FakeLogger() self.peer = peer self.neighbor = peer.neighbor self.negotiated = Negotiated(self.neighbor) self.connection = None if self.neighbor.connect: self.port = self.neighbor.connect elif os.environ.get('exabgp.tcp.port', '').isdigit(): self.port = int(os.environ.get('exabgp.tcp.port')) elif os.environ.get('exabgp_tcp_port', '').isdigit(): self.port = int(os.environ.get('exabgp_tcp_port')) else: self.port = 179 from exabgp.configuration.environment import environment self.log_routes = peer.neighbor.adj_rib_in or environment.settings( ).log.routes def fd(self): if self.connection is None: return None return self.connection.fd() # XXX: we use self.peer.neighbor.peer_address when we could use self.neighbor.peer_address def me(self, message): return "%s/%s %s" % (self.peer.neighbor.peer_address, self.peer.neighbor.peer_as, message) def accept(self, incoming): self.connection = incoming if self.peer.neighbor.api['neighbor-changes']: self.peer.reactor.processes.connected(self.peer.neighbor) # very important - as we use this function on __init__ return self def connect(self): # allows to test the protocol code using modified StringIO with a extra 'pending' function if not self.connection: local = self.neighbor.md5_ip.top( ) if not self.neighbor.auto_discovery else None peer = self.neighbor.peer_address.top() afi = self.neighbor.peer_address.afi md5 = self.neighbor.md5_password md5_base64 = self.neighbor.md5_base64 ttl_out = self.neighbor.ttl_out self.connection = Outgoing(afi, peer, local, self.port, md5, md5_base64, ttl_out) if not self.connection.init: yield False return if not local: self.neighbor.local_address = IP.create(self.connection.local) if self.neighbor.router_id is None and self.neighbor.local_address.afi == AFI.ipv4: self.neighbor.router_id = self.neighbor.local_address for connected in self.connection.establish(): if not connected: yield False continue if self.peer.neighbor.api['neighbor-changes']: self.peer.reactor.processes.connected(self.peer.neighbor) yield True return def close(self, reason='protocol closed, reason unspecified'): if self.connection: self.logger.debug(reason, self.connection.session()) # must be first otherwise we could have a loop caused by the raise in the below self.connection.close() self.connection = None self.peer.stats['down'] = self.peer.stats.get('down', 0) + 1 try: if self.peer.neighbor.api['neighbor-changes']: self.peer.reactor.processes.down(self.peer.neighbor, reason) except ProcessError: self.logger.debug( 'could not send notification of neighbor close to API', self.connection.session()) def _to_api(self, direction, message, raw): packets = self.neighbor.api['%s-packets' % direction] parsed = self.neighbor.api['%s-parsed' % direction] consolidate = self.neighbor.api['%s-consolidate' % direction] negotiated = self.negotiated if self.neighbor.api[ 'negotiated'] else None if consolidate: if packets: self.peer.reactor.processes.message(self.peer.neighbor, direction, message, negotiated, raw[:19], raw[19:]) else: self.peer.reactor.processes.message(self.peer.neighbor, direction, message, negotiated, b'', b'') else: if packets: self.peer.reactor.processes.packets(self.peer.neighbor, direction, int(message.ID), negotiated, raw[:19], raw[19:]) if parsed: self.peer.reactor.processes.message(message.ID, self.peer.neighbor, direction, message, negotiated, b'', b'') def write(self, message, negotiated=None): raw = message.message(negotiated) code = 'send-%s' % Message.CODE.short(message.ID) self.peer.stats[code] = self.peer.stats.get(code, 0) + 1 if self.neighbor.api.get(code, False): self._to_api('send', message, raw) for boolean in self.connection.writer(raw): yield boolean def send(self, raw): code = 'send-%s' % Message.CODE.short(ordinal(raw[18])) self.peer.stats[code] = self.peer.stats.get(code, 0) + 1 if self.neighbor.api.get(code, False): message = Update.unpack_message(raw[19:], self.negotiated) self._to_api('send', message, raw) for boolean in self.connection.writer(raw): yield boolean # Read from network ....................................................... def read_message(self): # This will always be defined by the loop but scope leaking upset scrutinizer/pylint msg_id = None packets = self.neighbor.api['receive-packets'] consolidate = self.neighbor.api['receive-consolidate'] parsed = self.neighbor.api['receive-parsed'] body, header = b'', b'' # just because pylint/pylama are getting more clever for length, msg_id, header, body, notify in self.connection.reader(): # internal issue if notify: code = 'receive-%s' % Message.CODE.NOTIFICATION.SHORT if self.neighbor.api.get(code, False): if consolidate: self.peer.reactor.processes.notification( self.peer.neighbor, 'receive', notify.code, notify.subcode, str(notify), None, header, body) elif parsed: self.peer.reactor.processes.notification( self.peer.neighbor, 'receive', notify.code, notify.subcode, str(notify), None, b'', b'') elif packets: self.peer.reactor.processes.packets( self.peer.neighbor, 'receive', msg_id, None, header, body) # XXX: is notify not already Notify class ? raise Notify(notify.code, notify.subcode, str(notify)) if not length: yield _NOP continue self.logger.debug( '<< message of type %s' % Message.CODE.name(msg_id), self.connection.session()) code = 'receive-%s' % Message.CODE.short(msg_id) self.peer.stats[code] = self.peer.stats.get(code, 0) + 1 for_api = self.neighbor.api.get(code, False) if for_api and packets and not consolidate: negotiated = self.negotiated if self.neighbor.api.get( 'negotiated', False) else None self.peer.reactor.processes.packets(self.peer.neighbor, 'receive', msg_id, negotiated, header, body) if msg_id == Message.CODE.UPDATE: if not self.neighbor.adj_rib_in and not ( for_api or self.log_routes) and not (parsed or consolidate): yield _UPDATE return try: message = Message.unpack(msg_id, body, self.negotiated) except (KeyboardInterrupt, SystemExit, Notify): raise except Exception as exc: self.logger.debug('could not decode message "%d"' % msg_id, self.connection.session()) self.logger.debug('%s' % str(exc), self.connection.session()) self.logger.debug(traceback.format_exc(), self.connection.session()) raise Notify( 1, 0, 'can not decode update message of type "%d"' % msg_id) # raise Notify(5,0,'unknown message received') if message.TYPE == Update.TYPE: if Attribute.CODE.INTERNAL_TREAT_AS_WITHDRAW in message.attributes: for nlri in message.nlris: nlri.action = IN.WITHDRAWN if for_api: negotiated = self.negotiated if self.neighbor.api.get( 'negotiated', False) else None if consolidate: self.peer.reactor.processes.message( msg_id, self.neighbor, 'receive', message, negotiated, header, body) elif parsed: self.peer.reactor.processes.message( msg_id, self.neighbor, 'receive', message, negotiated, b'', b'') if message.TYPE == Notification.TYPE: raise message if message.TYPE == Update.TYPE and Attribute.CODE.INTERNAL_DISCARD in message.attributes: yield _NOP else: yield message def validate_open(self): error = self.negotiated.validate(self.neighbor) if error is not None: raise Notify(*error) if self.neighbor.api['negotiated']: self.peer.reactor.processes.negotiated(self.peer.neighbor, self.negotiated) if self.negotiated.mismatch: self.logger.warning( '--------------------------------------------------------------------', self.connection.session()) self.logger.warning( 'the connection can not carry the following family/families', self.connection.session()) for reason, (afi, safi) in self.negotiated.mismatch: self.logger.warning( ' - %s is not configured for %s/%s' % (reason, afi, safi), self.connection.session()) self.logger.warning( 'therefore no routes of this kind can be announced on the connection', self.connection.session()) self.logger.warning( '--------------------------------------------------------------------', self.connection.session()) def read_open(self, ip): for received_open in self.read_message(): if received_open.TYPE == NOP.TYPE: yield received_open else: break if received_open.TYPE != Open.TYPE: raise Notify( 5, 1, 'The first packet received is not an open message (%s)' % received_open) self.logger.debug('<< %s' % received_open, self.connection.session()) yield received_open def read_keepalive(self): for message in self.read_message(): if message.TYPE == NOP.TYPE: yield message else: break if message.TYPE != KeepAlive.TYPE: raise Notify(5, 2) yield message # # Sending message to peer # def new_open(self): if self.neighbor.local_as: local_as = self.neighbor.local_as elif self.negotiated.received_open: local_as = self.negotiated.received_open.asn else: raise RuntimeError('no ASN available for the OPEN message') sent_open = Open( Version(4), local_as, self.neighbor.hold_time, self.neighbor.router_id, Capabilities().new(self.neighbor, self.peer._restarted)) # we do not buffer open message in purpose for _ in self.write(sent_open): yield _NOP self.logger.debug('>> %s' % sent_open, self.connection.session()) yield sent_open def new_keepalive(self, comment=''): keepalive = KeepAlive() for _ in self.write(keepalive): yield _NOP self.logger.debug( '>> KEEPALIVE%s' % (' (%s)' % comment if comment else ''), self.connection.session()) yield keepalive def new_notification(self, notification): for _ in self.write(notification): yield _NOP self.logger.debug( '>> NOTIFICATION (%d,%d,"%s")' % (notification.code, notification.subcode, notification.data), self.connection.session()) yield notification def new_update(self, include_withdraw): updates = self.neighbor.rib.outgoing.updates( self.neighbor.group_updates) number = 0 for update in updates: for message in update.messages(self.negotiated, include_withdraw): number += 1 for boolean in self.send(message): # boolean is a transient network error we already announced yield _NOP if number: self.logger.debug('>> %d UPDATE(s)' % number, self.connection.session()) yield _UPDATE def new_eor(self, afi, safi): eor = EOR(afi, safi) for _ in self.write(eor): yield _NOP self.logger.debug('>> EOR %s %s' % (afi, safi), self.connection.session()) yield eor def new_eors(self, afi=AFI.undefined, safi=SAFI.undefined): # Send EOR to let our peer know he can perform a RIB update if self.negotiated.families: families = self.negotiated.families if (afi, safi) == ( AFI.undefined, SAFI.undefined) else [ (afi, safi), ] for eor_afi, eor_safi in families: for _ in self.new_eor(eor_afi, eor_safi): yield _ else: # If we are not sending an EOR, send a keepalive as soon as when finished # So the other routers knows that we have no (more) routes to send ... # (is that behaviour documented somewhere ??) for eor in self.new_keepalive('EOR'): yield _NOP yield _UPDATE def new_operational(self, operational, negotiated): for _ in self.write(operational, negotiated): yield _NOP self.logger.debug('>> OPERATIONAL %s' % str(operational), self.connection.session()) yield operational def new_refresh(self, refresh): for _ in self.write(refresh, None): yield _NOP self.logger.debug('>> REFRESH %s' % str(refresh), self.connection.session()) yield refresh
class Connection (object): direction = 'undefined' identifier = {} def __init__ (self, afi, peer, local): # peer and local are strings of the IP try: self.defensive = environment.settings().debug.defensive self.logger = Logger() except RuntimeError: self.defensive = True self.logger = FakeLogger() self.afi = afi self.peer = peer self.local = local self.io = None self.established = False self.id = self.identifier.get(self.direction,1) def success (self): identifier = self.identifier.get(self.direction,1) + 1 self.identifier[self.direction] = identifier return identifier # Just in case .. def __del__ (self): if self.io: self.logger.warning('connection to %s closed' % self.peer,self.session()) self.close() def name (self): return "%s-%d %s-%s" % (self.direction,self.id,self.local,self.peer) def session (self): return "%s-%d" % (self.direction,self.id) def close (self): try: self.logger.warning('%s, closing connection' % self.name(),source=self.session()) if self.io: self.io.close() self.io = None except KeyboardInterrupt as exc: raise exc except Exception: pass def reading (self): while True: try: r,_,_ = select.select([self.io,],[],[],0) except select.error as exc: if exc.args[0] not in error.block: self.close() self.logger.warning('%s %s errno %s on socket' % (self.name(),self.peer,errno.errorcode[exc.args[0]]),self.session()) raise NetworkError('errno %s on socket' % errno.errorcode[exc.args[0]]) return False return r != [] def writing (self): while True: try: _,w,_ = select.select([],[self.io,],[],0) except select.error as exc: if exc.args[0] not in error.block: self.close() self.logger.warning('%s %s errno %s on socket' % (self.name(),self.peer,errno.errorcode[exc.args[0]]),self.session()) raise NetworkError('errno %s on socket' % errno.errorcode[exc.args[0]]) return False return w != [] def _reader (self, number): # The function must not be called if it does not return with no data with a smaller size as parameter if not self.io: self.close() raise NotConnected('Trying to read on a closed TCP connection') if number == 0: yield b'' return while not self.reading(): yield b'' data = b'' reported = '' while True: try: while True: if self.defensive and random.randint(0,2): raise socket.error(errno.EAGAIN,'raising network error on purpose') read = self.io.recv(number) if not read: self.close() self.logger.warning('%s %s lost TCP session with peer' % (self.name(),self.peer),self.session()) raise LostConnection('the TCP connection was closed by the remote end') data += read number -= len(read) if not number: self.logger.debug(LazyFormat('received TCP payload',data),self.session()) yield data return yield b'' except socket.timeout as exc: self.close() self.logger.warning('%s %s peer is too slow' % (self.name(),self.peer),self.session()) raise TooSlowError('Timeout while reading data from the network (%s)' % errstr(exc)) except socket.error as exc: if exc.args[0] in error.block: message = '%s %s blocking io problem mid-way through reading a message %s, trying to complete' % (self.name(),self.peer,errstr(exc)) if message != reported: reported = message self.logger.debug(message,self.session()) yield b'' elif exc.args[0] in error.fatal: self.close() raise LostConnection('issue reading on the socket: %s' % errstr(exc)) # what error could it be ! else: self.logger.critical('%s %s undefined error reading on socket' % (self.name(),self.peer),self.session()) raise NetworkError('Problem while reading data from the network (%s)' % errstr(exc)) def writer (self, data): if not self.io: # XXX: FIXME: Make sure it does not hold the cleanup during the closing of the peering session yield True return while not self.writing(): yield False self.logger.debug(LazyFormat('sending TCP payload',data),self.session()) # The first while is here to setup the try/catch block once as it is very expensive while True: try: while True: if self.defensive and random.randint(0,2): raise socket.error(errno.EAGAIN,'raising network error on purpose') # we can not use sendall as in case of network buffer filling # it does raise and does not let you know how much was sent number = self.io.send(data) if not number: self.close() self.logger.warning('%s %s lost TCP connection with peer' % (self.name(),self.peer),self.session()) raise LostConnection('lost the TCP connection') data = data[number:] if not data: yield True return yield False except socket.error as exc: if exc.args[0] in error.block: self.logger.debug( '%s %s blocking io problem mid-way through writing a message %s, trying to complete' % ( self.name(), self.peer, errstr(exc) ), self.session() ) yield False elif exc.errno == errno.EPIPE: # The TCP connection is gone. self.close() raise NetworkError('Broken TCP connection') elif exc.args[0] in error.fatal: self.close() self.logger.critical('%s %s problem sending message (%s)' % (self.name(),self.peer,errstr(exc)),self.session()) raise NetworkError('Problem while writing data to the network (%s)' % errstr(exc)) # what error could it be ! else: self.logger.critical('%s %s undefined error writing on socket' % (self.name(),self.peer),self.session()) yield False def reader (self): # _reader returns the whole number requested or nothing and then stops for header in self._reader(Message.HEADER_LEN): if not header: yield 0,0,b'',b'',None if not header.startswith(Message.MARKER): report = 'The packet received does not contain a BGP marker' yield 0,0,header,b'',NotifyError(1,1,report) return msg = ordinal(header[18]) length = unpack('!H',header[16:18])[0] if length < Message.HEADER_LEN or length > Message.MAX_LEN: report = '%s has an invalid message length of %d' % (Message.CODE.name(msg),length) yield length,0,header,b'',NotifyError(1,2,report) return validator = Message.Length.get(msg,lambda _: _ >= 19) if not validator(length): # MUST send the faulty length back report = '%s has an invalid message length of %d' % (Message.CODE.name(msg),length) yield length,0,header,b'',NotifyError(1,2,report) return number = length - Message.HEADER_LEN if not number: yield length,msg,header,b'',None return for body in self._reader(number): if not body: yield 0,0,b'',b'',None yield length,msg,header,body,None