Lab 6 Building an IP router

在这个 Lab 里我们要实现按转发表转发数据报的方法。我们不需要实现构造转发表的算法,也不需要用前缀树来快速查询转发表,所以实现起来不算难。

转发表需要对给定的 IP 给出下一跳的 IP 和对应的出接口/端口号,我们先来看看头文件和相应的新增转发表条目的方法:

class Router
{
public:
  // ...

private:
  std::pair<size_t, std::optional<Address>> find_best_route( const IPv4Datagram& dgram ) const;

  // The router's collection of network interfaces
  std::vector<std::shared_ptr<NetworkInterface>> interfaces_ {};

  struct ForwardEntry
  {
    uint32_t route_prefix {};
    uint8_t prefix_length {};
    std::optional<Address> next_hop = std::nullopt;
    size_t interface_num {};
  };
  std::vector<ForwardEntry> forward_table_ {};
};
void Router::add_route( const uint32_t route_prefix,
                        const uint8_t prefix_length,
                        const optional<Address> next_hop,
                        const size_t interface_num )
{
  forward_table_.push_back( { route_prefix, prefix_length, next_hop, interface_num } );
}

然后我们要实现按转发表转发数据的方法。一个路由器有多个端口,每个端口通常对应一个子网。这里的 NetworkInterface 是对端口的抽象,路由器则通过端口之间的数据交换实现了多个子网之间的数据交换。

我们可以用前缀树把查表速度优化为常数级,但 Lab 文档说 $O(N)$ 的查表速度是可以接受的,所以我这里就不优化了。

void Router::route()
{
  for ( auto& interface : interfaces_ ) {
    auto& datagram_received = interface->datagrams_received();

    while ( not datagram_received.empty() ) {
      auto dgram = std::move( datagram_received.front() );
      datagram_received.pop();

      const auto [interface_num, next_hop] = find_best_route( dgram );

      if ( dgram.header.ttl != 0 && --dgram.header.ttl != 0 && next_hop.has_value() ) {
        dgram.header.compute_checksum();
        interfaces_[interface_num]->send_datagram( std::move( dgram ), next_hop.value() );
      }
    }
  }
}

要注意左移 32 位是未定义行为,所以我们要特殊处理 prefix_length == 0 即默认路由的情况。

std::pair<size_t, std::optional<Address>> Router::find_best_route( const IPv4Datagram& dgram ) const
{
  size_t interface_num = 0;
  std::optional<Address> next_hop = std::nullopt;
  uint8_t longest_prefix_match = 0;

  for ( const auto& entry : forward_table_ ) {
    if ( entry.prefix_length < longest_prefix_match ) {
      continue;
    }
    const uint32_t mask = ( entry.prefix_length == 0 ) ? 0 : ( UINT32_MAX << ( 32 - entry.prefix_length ) );
    if ( ( entry.prefix_length == 0 ) || ( ( dgram.header.dst & mask ) == ( entry.route_prefix & mask ) ) ) {
      // The range of left shift counts is limited in 0-31, so `mask` is not reliable when
      // entry.prefix_length == 0, which means the entry is the default route, so we do not use `mask` when
      // prefix_length == 0
      interface_num = entry.interface_num;
      longest_prefix_match = entry.prefix_length;
      next_hop = entry.next_hop.value_or( Address::from_ipv4_numeric( dgram.header.dst ) );
    }
  }
  return { interface_num, next_hop };
}