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 logfunc.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() log.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: log.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() log.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: log.critical( '%s %s undefined error writing on socket' % (self.name(), self.peer), self.session()) yield False
def unpack_nlri(cls, afi, safi, data, action, addpath): a, s = AFI.create(afi), SAFI.create(safi) logfunc.debug(lazynlri(a, s, addpath, data), 'parser') key = '%s/%s' % (a, s) if key in cls.registered_nlri: return cls.registered_nlri[key].unpack_nlri( a, s, data, action, addpath) raise Notify(3, 0, 'trying to decode unknown family %s/%s' % (a, s))
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() log.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: logfunc.debug(lazyformat('received TCP payload', data), self.session()) yield data return yield b'' except socket.timeout as exc: self.close() log.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 log.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: log.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 parse(self, data, direction, negotiated): if not data: return self try: # We do not care if the attribute are transitive or not as we do not redistribute flag = Attribute.Flag(data[0]) aid = Attribute.CODE(data[1]) except IndexError: self.add(TreatAsWithdraw()) return self try: offset = 3 length = data[2] if flag & Attribute.Flag.EXTENDED_LENGTH: offset = 4 length = (length << 8) + data[3] except IndexError: self.add(TreatAsWithdraw(aid)) return self data = data[offset:] left = data[length:] attribute = data[:length] logfunc.debug(lazyattribute(flag, aid, length, data[:length]), 'parser') # remove the PARTIAL bit before comparaison if the attribute is optional if aid in Attribute.attributes_optional: flag &= Attribute.Flag.MASK_PARTIAL & 0xFF # flag &= ~Attribute.Flag.PARTIAL & 0xFF # cleaner than above (python use signed integer for ~) if aid in self: if aid in self.NO_DUPLICATE: raise Notify(3, 1, 'multiple attribute for %s' % str(Attribute.CODE(attribute.ID))) log.debug( 'duplicate attribute %s (flag 0x%02X, aid 0x%02X) skipping' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser', ) return self.parse(left, direction, negotiated) # handle the attribute if we know it if Attribute.registered(aid, flag): if length == 0 and aid not in self.VALID_ZERO: self.add(TreatAsWithdraw(aid)) return self.parse(left, direction, negotiated) try: decoded = Attribute.unpack(aid, flag, attribute, direction, negotiated) except IndexError as exc: if aid in self.TREAT_AS_WITHDRAW: decoded = TreatAsWithdraw(aid) else: raise exc except Notify as exc: if aid in self.TREAT_AS_WITHDRAW: decoded = TreatAsWithdraw() elif aid in self.DISCARD: decoded = Discard() else: raise exc self.add(decoded) return self.parse(left, direction, negotiated) # XXX: FIXME: we could use a fallback function here like capability # if we know the attribute but the flag is not what the RFC says. if aid in Attribute.attributes_known: if aid in self.TREAT_AS_WITHDRAW: log.debug( 'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) treat as withdraw' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser', ) self.add(TreatAsWithdraw()) if aid in self.DISCARD: log.debug( 'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) discard' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser', ) return self.parse(left, direction, negotiated) # XXX: Check if we are missing any log.debug( 'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) unspecified (should not happen)' % (Attribute.CODE.names.get(aid, 'unset'), flag, aid), 'parser', ) return self.parse(left, direction, negotiated) # it is an unknown transitive attribute we need to pass on if flag & Attribute.Flag.TRANSITIVE: log.debug('unknown transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag, aid), 'parser') try: decoded = GenericAttribute(aid, flag | Attribute.Flag.PARTIAL, attribute) except IndexError: decoded = TreatAsWithdraw(aid) self.add(decoded, attribute) return self.parse(left, direction, negotiated) # it is an unknown non-transitive attribute we can ignore. log.debug('ignoring unknown non-transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag, aid), 'parser') return self.parse(left, direction, negotiated)
def _main(self): """yield True if we want to come back to it asap, None if nothing urgent, and False if stopped""" if self._teardown: raise Notify(6, 3) self.neighbor.rib.incoming.clear() include_withdraw = False # Announce to the process BGP is up log.info('connected to %s with %s' % (self.id(), self.proto.connection.name()), 'reactor') self.stats['up'] = self.stats.get('up', 0) + 1 if self.neighbor.api['neighbor-changes']: try: self.reactor.processes.up(self.neighbor) except ProcessError: # Can not find any better error code than 6,0 ! # XXX: We can not restart the program so this will come back again and again - FIX # XXX: In the main loop we do exit on this kind of error raise Notify(6, 0, 'ExaBGP Internal error, sorry.') send_eor = not self.neighbor['manual-eor'] new_routes = None self._resend_routes = SEND.NORMAL send_families = [] # Every last asm message should be re-announced on restart for family in self.neighbor.asm: if family in self.neighbor.families(): self.neighbor.messages.appendleft(self.neighbor.asm[family]) operational = None refresh = None command_eor = None number = 0 refresh_enhanced = True if self.proto.negotiated.refresh == REFRESH.ENHANCED else False send_ka = KA(self.proto.connection.session, self.proto) while not self._teardown: for message in self.proto.read_message(): self.recv_timer.check_ka(message) if send_ka() is not False: # we need and will send a keepalive while send_ka() is None: yield ACTION.NOW # Received update if message.TYPE == Update.TYPE: number += 1 log.debug('<< UPDATE #%d' % number, self.id()) for nlri in message.nlris: self.neighbor.rib.incoming.update_cache(Change(nlri, message.attributes)) logfunc.debug(lazyformat(' UPDATE #%d nlri ' % number, nlri, str), self.id()) elif message.TYPE == RouteRefresh.TYPE: if message.reserved == RouteRefresh.request: self._resend_routes = SEND.REFRESH send_families.append((message.afi, message.safi)) # SEND OPERATIONAL if self.neighbor['capability']['operational']: if not operational: new_operational = self.neighbor.messages.popleft() if self.neighbor.messages else None if new_operational: operational = self.proto.new_operational(new_operational, self.proto.negotiated) if operational: try: next(operational) except StopIteration: operational = None # make sure that if some operational message are received via the API # that we do not eat memory for nothing elif self.neighbor.messages: self.neighbor.messages.popleft() # SEND REFRESH if self.neighbor['capability']['route-refresh']: if not refresh: new_refresh = self.neighbor.refresh.popleft() if self.neighbor.refresh else None if new_refresh: refresh = self.proto.new_refresh(new_refresh) if refresh: try: next(refresh) except StopIteration: refresh = None # Take the routes already sent to that peer and resend them if self._reconfigure: self._reconfigure = False # we are here following a configuration change if self._neighbor: # see what changed in the configuration self.neighbor.rib.outgoing.replace(self._neighbor.backup_changes, self._neighbor.changes) # do not keep the previous routes in memory as they are not useful anymore self._neighbor.backup_changes = [] # Take the routes already sent to that peer and resend them if self._resend_routes != SEND.DONE: enhanced = True if refresh_enhanced and self._resend_routes == SEND.REFRESH else False self._resend_routes = SEND.DONE self.neighbor.rib.outgoing.resend(send_families, enhanced) send_families = [] # Need to send update if not new_routes and self.neighbor.rib.outgoing.pending(): # XXX: in proto really. hum to think about ? new_routes = self.proto.new_update(include_withdraw) if new_routes: count = 1 if self.neighbor['rate-limit'] > 0 else 25 try: for _ in range(count): # This can raise a NetworkError next(new_routes) except StopIteration: new_routes = None include_withdraw = True elif send_eor: send_eor = False for _ in self.proto.new_eors(): yield ACTION.NOW log.debug('>> EOR(s)', self.id()) # SEND MANUAL KEEPALIVE (only if we have no more routes to send) elif not command_eor and self.neighbor.eor: new_eor = self.neighbor.eor.popleft() command_eor = self.proto.new_eors(new_eor.afi, new_eor.safi) if command_eor: try: next(command_eor) except StopIteration: command_eor = None if new_routes or message.TYPE != NOP.TYPE: yield ACTION.NOW elif self.neighbor.messages or operational: yield ACTION.NOW elif self.neighbor.eor or command_eor: yield ACTION.NOW else: yield ACTION.LATER # read_message will loop until new message arrives with NOP if self._teardown: break # If graceful restart, silent shutdown if self.neighbor['capability']['graceful-restart'] and self.proto.negotiated.sent_open.capabilities.announced( Capability.CODE.GRACEFUL_RESTART ): log.error('closing the session without notification', self.id()) self.proto.close('graceful restarted negotiated, closing without sending any notification') raise NetworkError('closing') # notify our peer of the shutdown raise Notify(6, self._teardown)
def unpack_message(cls, data, direction, negotiated): logfunc.debug(lazyformat('parsing UPDATE', data), 'parser') length = len(data) # This could be speed up massively by changing the order of the IF if length == 4 and data == b'\x00\x00\x00\x00': return EOR(AFI.ipv4, SAFI.unicast) # pylint: disable=E1101 if length == 11 and data.startswith(EOR.NLRI.PREFIX): return EOR.unpack_message(data, direction, negotiated) withdrawn, _attributes, announced = cls.split(data) if not withdrawn: log.debug('withdrawn NLRI none', 'routes') attributes = Attributes.unpack(_attributes, direction, negotiated) if not announced: log.debug('announced NLRI none', 'routes') # Is the peer going to send us some Path Information with the route (AddPath) if direction == Direction.IN: addpath = negotiated.addpath.receive(AFI.ipv4, SAFI.unicast) else: addpath = negotiated.addpath.send(AFI.ipv4, SAFI.unicast) # empty string for NoNextHop, the packed IP otherwise (without the 3/4 bytes of attributes headers) nexthop = attributes.get(Attribute.CODE.NEXT_HOP, NoNextHop) # nexthop = NextHop.unpack(_nexthop.ton()) # XXX: NEXTHOP MUST NOT be the IP address of the receiving speaker. nlris = [] while withdrawn: nlri, left = NLRI.unpack_nlri(AFI.ipv4, SAFI.unicast, withdrawn, IN.WITHDRAWN, addpath) log.debug('withdrawn NLRI %s' % nlri, 'routes') withdrawn = left nlris.append(nlri) while announced: nlri, left = NLRI.unpack_nlri(AFI.ipv4, SAFI.unicast, announced, IN.ANNOUNCED, addpath) nlri.nexthop = nexthop log.debug('announced NLRI %s' % nlri, 'routes') announced = left nlris.append(nlri) unreach = attributes.pop(MPURNLRI.ID, None) reach = attributes.pop(MPRNLRI.ID, None) if unreach is not None: nlris.extend(unreach.nlris) if reach is not None: nlris.extend(reach.nlris) if not attributes and not nlris: # Careful do not use == or != as the comparaison does not work if unreach is None and reach is None: return EOR(AFI.ipv4, SAFI.unicast) if unreach is not None: return EOR(unreach.afi, unreach.safi) if reach is not None: return EOR(reach.afi, reach.safi) raise RuntimeError('This was not expected') update = Update(nlris, attributes) def parsed(_): # we need the import in the function as otherwise we have an cyclic loop # as this function currently uses Update.. from exabgp.reactor.api.response import Response from exabgp.version import json as json_version return 'json %s' % Response.JSON(json_version).update(negotiated.neighbor, 'receive', update, None, '', '') logfunc.debug(lazyformat('decoded UPDATE', '', parsed), 'parser') return update