Skip to content

Instantly share code, notes, and snippets.

@nuta
Last active September 5, 2023 10:46
Show Gist options
  • Save nuta/ae5de234baaad580dac4 to your computer and use it in GitHub Desktop.
Save nuta/ae5de234baaad580dac4 to your computer and use it in GitHub Desktop.

lwIPの移植

修正BSDライセンスでかなり良い感じのTCP/IPプロトコルスタック。 MinixもVirtualBoxも使っている様子。主な実装済みプロトコルは以下の通り。

  • DNS, DHCP
  • TCP, UDP
  • IPv4, IPv6, ICMP
  • ARP

最低限必要なもの

移植はかなり簡単で以下のものを用意すれば OSのTCP/IPプロトコルスタックとしては十分に動いてくれる。

標準Cライブラリ関数

int memcmp(const void *s1, const void *s2, size_t n);
void *memcpy(void *dest, const void *src, size_t n);
void *memset(void *s, int c, size_t n);
size_t strlen(const char *s);

malloc()がないのは、lwIPが.dataセクションかどこかに メモリプールを用意して自前でやりくりするため。

sys_now

sys_now()は現在時刻(ミリ秒)を返す。時刻の差をとるために使うので ラップアラウンドへの配慮は不要。定数返すだけでも動くが、その実装だとたぶん TCP timestampとかタイムアウトとかが動かないと思う。

ヘッダファイルたち

/* lwipopts.h: 自分でファイルを作る */
#define NO_SYS 1

/* cc.h */
#define LWIP_PLATFORM_DIAG(x)   printf  // デバッグ情報
#define LWIP_PLATFORM_ASSERT(x) printf  // assertion失敗

NICのデバイスドライバ

NICのドライバはsrc/netif/ethernetif.cに雛形があるのでそれを以下の ように修正する。下の2つはソースコードに丁寧なコメントが載っている。

  • low_level_init(): netif->hwaddrにMACアドレスを詰める。
  • ethernetif_input(): パケットが来たら、これを呼ぶ。引数にはそのNICのnetif
  • low_level_output(): パケットをひとつずつ真心込めて送信
  • low_level_input(): 受信したパケットをバッファに詰めていく。ethernetif_input()から呼ばれる。

その他

マルチスレッドサポート、BSDソケットのエミュレーションをlwIPに やってもらうにはSemaphoreとかも必要。参照: doc/sys_arch.txt

セットアップ

/* たぶんこんなにインクルードしなくていい */
#include <lwip/opt.h>
#include <lwip/stats.h>
#include <lwip/sys.h>
#include <lwip/pbuf.h>
#include <lwip/udp.h>
#include <lwip/tcp.h>
#include <lwip/dns.h>
#include <lwip/dhcp.h>
#include <lwip/init.h>
#include <lwip/netif.h>
#include <netif/etharp.h>

static struct netif netif;

void setup(void) {
  struct ip_addr ipaddr;
  struct ip_addr netmask;
  struct ip_addr gateway;

  /* DHCPも対応しているが、ここでは静的に割り当てる。 */
  IP4_ADDR(&netmask, 255, 255, 255, 255);
  IP4_ADDR(&gateway,  10,   0,   2,   2);
  IP4_ADDR(&ipaddr,   10,   0,   2,  15);

  lwip_init(); // いろいろ初期化

  /* NICを登録 */
  netif_add(&netif, &ipaddr, &netmask, &gateway, NULL,
            (netif_init_fn) ethernetif_init, ethernet_input);

  /*
   *  デフォルトルートに設定する
   *
   *  lwIPは複数NICに対応しており、IPパケット送信時に
   *  ip_route() でどのNICから送るべきかを決定されるが、
  *  送信先がどのNICのセグメントでもない場合に、これで
   *  設定されたNICが選ばれる。
   *
   */
  netif_set_default(&netif);

  /* NICの有効化: 準備完了なことをlwIPに伝える */
  netif_set_up(&netif);
}

TCP/IP (クライアント)

エラー処理は全くしていない。パケットの送受信はサーバの方を参照。

/* データを受信した時に呼ばれる */
static err_t recv(void *arg, struct tcp_pcb *pcb,
                  struct pbuf *pbuf, err_t err) {

  /* 受信したものは pbuf の中 */
  puts("received!");
  return ERR_OK;
}


/* データがきちんと送信できた時に呼ばれる */
err_t sent (void *arg, struct tcp_pcb *pcb, u16_t len){
  return ERR_OK;
}


/* コネクションが張られたときに呼ばれる */
err_t connected(void *arg, struct tcp_pcb *pcb, err_t err) {

  tcp_recv(pcb, recv); // 受信した時のハンドラを設定
  tcp_sent(pcb, sent); // 送信できた時のハンドラを設定

  /*
   *  ここでパケットを送る
   *
   *    tcp_write()   送信バッファにデータを詰める
   *    tcp_output()  送信開始
   *
   */

  return ERR_OK;
}

void main(void) {
  struct tcp_pcb *pcb;
  struct ip_addr ipaddr;
  u16_t port;

  IP4_ADDR(&ipaddr, 130, 158, 87, 1); /* www.coins.tsukuba.ac.jp */
  port = 80;
  pcb = tcp_new();
  tcp_connect(pcb, &ipaddr, port, connected);
}

TCP/IP (サーバ)

たぶん動くechoサーバ。クライアントと同じく、エラー処理はしていない。

err_t recv(void *arg, struct tcp_pcb *pcb,
           struct pbuf *pbuf, err_t err) {

  if (pbuf != NULL) {
    char buf[512];
    int len;

    /* 受信したものを受け取る */
    len = (pbuf->tot_len < sizeof(buf))? pbuf->tot_len : sizeof(buf);
    memcpy(&buf, pbuf->payload, len);
    tcp_recved(pcb, pbuf->tot_len); // 受け取ったことを通知
    pbuf_free(pbuf); // pbuf開放

    /* 適当に返信を送る */
    tcp_write(pcb, len, size, 1);
    tcp_output(pcb);


  }else{
    tcp_close(pcb);
  }

  return ERR_OK;
}


/* クライアントからコネクションを張られたときに呼ばれる */
err_t accept(void *arg, struct tcp_pcb *pcb, err_t err) {

  tcp_setprio(pcb, TCP_PRIO_MIN); // 優先度を設定
  tcp_recv(pcb, recv);            // パケットを受信した時のハンドラを設定
  tcp_err(pcb, NULL);             // 何か起きた時のハンドラを設定
  tcp_poll(pcb, NULL, 4);         // インターバルごとに呼ばれるハンドラを設定
  return ERR_OK;
}


void main(void) {
  struct tcp_pcb *pcb;

  pcb = tcp_new();

  /* bind */
  tcp_bind(pcb, IP_ADDR_ANY /* IPアドレス */, 80 /* ポート番号 */);

  /* listen: 注意点として、pcbが新しく割り当てられて戻ってくるので
             戻り値をきちんと拾ってあげる必要がある */
  pcb = tcp_listen(pcb);

  /* accept: これはブロックしない */
  tcp_accept(server->pcb, accept_handler);

  /* パケットを待ち続ける */
  for (;;);
}

注意点

  • APIはちゃんとマニュアル・ソースコードを読む。BSDソケットと同じ名前でも 振る舞いが全く異なる場合がある。
  • lwIPのassertionが失敗するのは、ほとんどの場合で使い方 or NICのドライバが間違ってるのが原因。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment