うたカモ技術ブログ

Linux OpenWrt

OpenWrtアプリケーション開発   C言語のUBUSサーバーと連携するLuCI用プラグインの作成と追加(難易度Lv3)

post: 

この記事で紹介するソースコードは自由に使っていただいて構いません。 アプリケーションの開発や自己学習にお役立て下さい。ただし、当ブログでは掲載するソースコードを流用・利用したことによる損害等につきましては 一切の責任を負いません。自己責任で利用お願いします。

この記事では、C言語で作成したUBUS対応アプリ(UBUSサーバー)をLuCIから呼び出すプラグインを作成します。 UBUSはOpenWrtのプロセス同士が要求や応答をやり取りする仕組みです。 前回のluci-app-sample02に続き、今回は常駐するCプログラムへ処理を渡します。

題材はネットワークインタフェースの情報取得とMTU変更です。 C側で使うioctl()は、ファイル記述子を通してカーネルに情報取得や設定変更を要求するシステムコールです。 このサンプルではソケットを開き、インタフェース名と操作に対応した要求番号を渡します。 ドライバやカーネルモジュールを新しく作る必要はありません。

実装する機能は「IPv4アドレスを持つインタフェースの一覧」「IPv4アドレス取得」「MTU取得」「MTU変更」の4つです。 MTUは、そのインタフェースで扱うIPパケットの最大サイズを表します。 取得と変更を1つずつ追いながら、C・UBUS・Lua・Web画面のつながりを確認していきます。

この記事の位置付け

この記事は、次のような方に向けています。

  • LuCIの画面から、C言語で作成したプログラムを呼び出したい方
  • UBUSサーバーとLuCIを連携させる方法を知りたい方
  • ioctlを使ってネットワークインタフェースの情報取得や設定変更を試したい方

本記事はLuCIプラグイン開発の難易度Lv3に相当します。前提となるページ追加とスクリプト連携は、以下の記事で紹介しています。

目次

  1. 実施環境と補足事項について
  2. サンプルプログラムのダウンロード案内
  3. 今回作成するUBUSサーバーの概要
  4. そもそもioctlとは何か
  5. 処理の流れと4つのメソッド
  6. 本題:C言語でIOCTL処理とUBUSサーバーを作る
  7. LuCIのコントローラーと画面を作る
  8. パッケージをビルドして起動する
  9. コンソールとLuCIで動作を確認する
  10. おわりに
  11. 参考文献

実施環境と補足事項について

この記事は以下の環境で実施した結果を元に作成しています。

#開発用PCの実行環境OS
ubuntu 22.04 LTS 64bit ※今回はUbuntuで開発しましたが、Windows(WSL2)上でもOKです。
#OpenWrtデバイス
  名称:Raspberry Pi3 Model B
  CPU:ARM Cortex-A53 (1.2GHz)
  SOC:Broadcom BCM2837
  RAM:1GB
  ストレージ(media):MicroSDカード
#OpenWrt
  OpenWrt 24.10.0
主なパッケージ
  luci
  luci-compat、luci-lua-runtime
  libubus-lua

今回の機能はC言語のプログラムで実現するため、使用するOpenWrtデバイス向けにプログラムをビルドする必要があります。 この記事ではRaspberry Pi3Bを使用しますが、LuCIがインストールされた他のデバイスでも、それぞれのデバイス向けにビルドすることで手順を試せます。

本文の設定・コマンドとLuCI操作画面は、OpenWrt 24.10.0を基準にしています。 Lua形式のコントローラーと画面を動かすため、LuCIのLua互換機能を利用します。 メニューや画面の外観はバージョンやテーマによって異なります。

[補足事項]
記事内で紹介するコンソール表記は次の通りです。

開発用PCのコンソール表記
ユーザー名とカレントディレクトリを色付けして表記します。
user:~/openwrt$ command
OpenWrtのコンソール表記
ユーザー名はroot@OpenWrtです。
root@OpenWrt:~# command

この記事が想定するOpenWrtデバイスの初期状態

この記事で想定するOpenWrtデバイスの状態は、OpenWrtがインストールされた直後の初期状態です。
IPアドレスに着目すれば、OpenWrtデバイス自身は192.168.1.1/24のIPアドレスを持ち、192.168.1.0/24のネットワークを管理しています。

サンプルプログラムのダウンロード案内

今回取り上げるLuCI用プラグイン一式は私のGitHubリポジトリにあります。 パッケージ作成に必要なMakefileも揃った完全版です。

一応、この記事の内容に沿って1つずつ作業をしていけば、このリポジトリにあるものと同じものが作成できます。

記述ミスなどで躓いたら、以下のリポジトリをダウンロードして手っ取り早くビルド・インストールして動作を確認してみてください。

今回作成するUBUSサーバーの概要

今回作成するアプリは、C言語で実装したUBUSサーバーです。 ネットワークインタフェースの情報取得やMTU変更の要求をUBUS経由で受け付け、ioctl()を使って処理します。

あわせてLuCI用のプラグインを作成し、ブラウザーからインタフェースの一覧やIPv4アドレス、MTUを確認できるようにします。 画面からMTUを変更することもできます。 後の節では、画面からアプリへ要求が届くまでの流れと、アプリが提供する4つのメソッドを紹介します。

今回紹介するUBUSサーバーは前回記事の「自作スクリプトと連携するLuCI用プラグインの作成と追加(難易度Lv2)自作スクリプトと連携するLuCI用プラグインの作成と追加(難易度Lv2)」と違うのは、プラグインのコア機能として実行されるUBUSサーバーがioctlを通してLinuxのカーネルモジュールまたはドライバーモジュールに対して直接要求できる点です。これにより、ユーザー空間のソフトウェアとしてはカーネル空間上の制御機能との接点を持つことができ、より詳細なハードウェア・システム情報や設定変更が可能になります。

Web画面からLuCIとUBUSを経てCプログラムがioctlでカーネルに要求を渡し、要求先によってはドライバーを介してハードウェアにアクセスする流れ

今回のUBUSサーバー開発とLuCI用プラグイン(UI)を理解すれば、OpenWrt上のソフトウェア開発としてはかなりのことができるようになります。後はアイデア次第です。

余談

実は、今回の記事の内容は既存のC言語によるUBUSサーバーの作成記事と、 LuCI用カスタムページの作成と追加の記事の2つで十分カバーできます。

それでもこの記事を書いたのは、Lv1から難易度を分け、段階を踏みながらOpenWrt上のさまざまなアプリ開発方法を小シリーズとして紹介するのも、ある程度理にかなっているかなと思ったからになります。

そもそもioctlとは何か

ioctl()は、ユーザー空間のプログラムからLinuxカーネルへ情報の取得や設定変更などを要求するシステムコールです。 開いているファイルやソケットを示すファイル記述子と、操作を指定する要求番号(IOCTLリクエスト)、必要なデータを渡して呼び出します。

ioctlでソケットのファイル記述子、要求番号、インタフェース名を渡し、カーネルからMTUを取得する流れ

要求番号には、Linuxのネットワーク機能などで共通に使われるものがあります。 一方、デバイスドライバーが独自の操作を公開する場合は、そのドライバーに対応した要求番号やデータ形式が定義されている必要があります。 独自の操作を使うには、ドライバーが受け付ける内容を確認してプログラムから指定します。

この記事ではネットワークインタフェースを操作するため、ソケットを開いてioctl()を呼び出します。 共通の要求番号であるSIOCGIFMTUでMTUを取得し、SIOCSIFMTUで変更を要求します。 UBUSはLuCIとC言語のプログラムをつなぎ、C言語のプログラムがioctl()を通じてカーネル側に要求を渡します。

このようにioctl()は、ユーザー空間のプログラムがカーネル側の機能に要求を渡す接点です。 要求先によってはドライバーを通じてハードウェアの状態を取得・制御できるため、一般的なWeb画面やGUIの表示処理よりも、システムの内部に近い部分を扱います。

処理の流れと4つのメソッド

  1. 画面のボタンを押すと、JavaScriptがメソッド名と引数をLuCIへPOSTします。
  2. Luaコントローラーが入力を確認し、UBUSオブジェクト ioctl-tool を呼び出します。
  3. CのUBUSサーバーが ioctl() を実行し、結果をUBUSへ返します。
  4. LuaがJSONで応答し、画面の Action Result に表示されます。
Set MTUの操作がブラウザーからLua、UBUS、Cプログラムを通じてカーネルへ届き、変更後のMTUが画面に戻る流れ
UBUSメソッド引数使うIOCTL処理
list_ifなしSIOCGIFCONFIPv4アドレスを持つインタフェースの一覧
get_if_ipv4ifnameSIOCGIFADDR指定したインタフェースのIPv4アドレス取得
get_mtuifnameSIOCGIFMTU現在のMTU取得
set_mtuifname、mtuSIOCSIFMTUMTU変更後に現在値を取得して返す

SIOCGIFCONFが返すのはIPv4アドレスの一覧です。 IPv4を割り当てていないインタフェースやIPv6だけのインタフェースも含めた、全デバイスの一覧ではありません。 一覧に出ないインタフェースのMTUも、名前を直接指定すれば取得できます。

本題:C言語でIOCTL処理とUBUSサーバーを作る

1.OpenWrtの開発環境を整備する

まず、OpenWrtデバイスと開発用PCを用意します。準備の手順は、前回記事の「本題:開発準備」を参照してください。 今回はC言語のプログラムをビルドするため、使用するOpenWrtデバイスに対応したbuildrootも必要です。

buildrootの準備方法はOpenWrtのbuildroot構築記事で紹介しています。 このサンプルでは、インストール先と同じOpenWrt 24.10.0を使うため、リポジトリの取得後に次のタグを選択してください。 既に対応するbuildrootを用意している場合は、そのディレクトリで作業を進めます。

kamo@pc:~$ cd ~/openwrt
kamo@pc:~/openwrt$ git checkout v24.10.0

2.パッケージ用のディレクトリを作る

まず、開発用PC上にパッケージの作業ディレクトリを用意します。ここでは ~/src/luci-app-sample03 を使います。以降、作成するファイルのパスは、このディレクトリを基準に記載します。

同じ場所に別の luci-app-sample03 がある場合は、既存のファイルを確認してください。次のコマンドはディレクトリを作成し、その中へ移動します。

mkdir -p "$HOME/src/luci-app-sample03/files/src/bin" \
    "$HOME/src/luci-app-sample03/files/etc/init.d" \
    "$HOME/src/luci-app-sample03/files/luci/controller" \
    "$HOME/src/luci-app-sample03/files/luci/view/luci-app-sample03"
cd "$HOME/src/luci-app-sample03"

この後に作成するファイルの配置は次の通りです。大文字の Makefile はパッケージ用、小文字の makefile はCプログラムのビルド用です。

luci-app-sample03/
├── Makefile
└── files/
    ├── src/bin/
    │   ├── ioctl_tool.h
    │   ├── ioctl_tool.c
    │   ├── ubus_server.c
    │   └── makefile
    ├── etc/init.d/luci-app-sample03
    └── luci/
        ├── controller/module.lua
        └── view/luci-app-sample03/ioctl.htm

以下では各ファイルの全文を掲載します。nano ファイル名などのエディタで新規作成し、それぞれのコードを貼り付けて保存してください。nanoでは Ctrl+O、Enterで保存し、Ctrl+Xで終了できます。

手入力せずに試す場合は、ソース一式のアーカイブも利用できます。 例えばブラウザーで ~/Downloads に保存した場合は、次のコマンドで内容を確認してから展開できます。 先ほど作成した空のディレクトリには展開できますが、既にファイルを作成した場合は上書きしないよう注意してください。

tar -tzf "$HOME/Downloads/luci-app-sample03-ioctl.tar.gz"
tar -xzf "$HOME/Downloads/luci-app-sample03-ioctl.tar.gz" -C "$HOME/src"
cd "$HOME/src/luci-app-sample03"

展開した場合は、各ファイルの作成を省略して「パッケージをビルドして起動する」の3番から進めます。 以下のコードと説明は、処理を確認したいときに参照してください。

3.共通の宣言を記述する:ioctl_tool.h

files/src/bin/ioctl_tool.h を作成します。このヘッダーファイルには、IOCTLを使う4つの関数の宣言と、このサンプルで受け付けるMTUの範囲をまとめます。

nano files/src/bin/ioctl_tool.h
/*
 * Copyright (C) 2024 utakamo <contact@utakamo.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#ifndef SAMPLE03_IOCTL_TOOL_H
#define SAMPLE03_IOCTL_TOOL_H

#include <stddef.h>
#include <net/if.h>

/* This sample accepts MTUs in this range; the driver may restrict it further. */
#define SAMPLE_MTU_MIN 68
#define SAMPLE_MTU_MAX 65536

int list_if(struct ifreq **interfaces, size_t *count);
int get_if_ipv4(const char *ifname, char *address, size_t size);
int get_mtu(const char *ifname, int *mtu);
int set_mtu(const char *ifname, int mtu);

#endif

SAMPLE_MTU_MINとSAMPLE_MTU_MAXは、このプログラム側の入力制限です。実際に設定できる値は対象インタフェースにも依存します。list_if()の結果は呼び出し元で解放します。

4.IOCTLによる情報取得とMTU変更を実装する:ioctl_tool.c

次に files/src/bin/ioctl_tool.c を作成します。インタフェースのIPv4一覧・IPv4アドレス・MTUを取得する処理と、MTUを変更する処理をまとめています。

nano files/src/bin/ioctl_tool.c
/*
 * Copyright (C) 2024 utakamo <contact@utakamo.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <arpa/inet.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include "ioctl_tool.h"

/* Preserve ioctl's errno across close(), so the caller can report the cause. */
static int call_ioctl(unsigned long command, struct ifreq *request)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0)
        return -1;
    int result = ioctl(fd, command, request);
    int saved_errno = errno;
    close(fd);
    errno = saved_errno;
    return result;
}

static int prepare_request(struct ifreq *request, const char *ifname)
{
    if (!ifname || !*ifname || strlen(ifname) >= IFNAMSIZ) {
        errno = EINVAL;
        return -1;
    }
    memset(request, 0, sizeof(*request));
    memcpy(request->ifr_name, ifname, strlen(ifname) + 1);
    return 0;
}

/* SIOCGIFCONF lists IPv4 address entries, not every network device. */
int list_if(struct ifreq **interfaces, size_t *count)
{
    struct ifconf config = {0};
    *interfaces = NULL;
    *count = 0;
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0)
        return -1;
    if (ioctl(fd, SIOCGIFCONF, &config) < 0)
        goto fail;
    if (config.ifc_len < 0 || config.ifc_len > INT_MAX - (int)sizeof(struct ifreq)) {
        errno = EOVERFLOW;
        goto fail;
    }
    int capacity = config.ifc_len + sizeof(struct ifreq);
    config.ifc_buf = malloc(capacity);
    if (!config.ifc_buf)
        goto fail;
    config.ifc_len = capacity;
    if (ioctl(fd, SIOCGIFCONF, &config) < 0)
        goto fail;
    /* An address may have appeared between the two calls. Ask the user to retry. */
    if (capacity - config.ifc_len < (int)sizeof(struct ifreq)) {
        errno = EAGAIN;
        goto fail;
    }
    *interfaces = config.ifc_req;
    *count = config.ifc_len / sizeof(struct ifreq);
    close(fd);
    return 0;

fail:;
    int saved_errno = errno;
    free(config.ifc_buf);
    close(fd);
    errno = saved_errno;
    return -1;
}

int get_if_ipv4(const char *ifname, char *address, size_t size)
{
    struct ifreq request;
    if (prepare_request(&request, ifname) < 0 || call_ioctl(SIOCGIFADDR, &request) < 0)
        return -1;
    const struct sockaddr_in *value = (const struct sockaddr_in *)&request.ifr_addr;
    return inet_ntop(AF_INET, &value->sin_addr, address, size) ? 0 : -1;
}

int get_mtu(const char *ifname, int *mtu)
{
    struct ifreq request;
    if (prepare_request(&request, ifname) < 0 || call_ioctl(SIOCGIFMTU, &request) < 0)
        return -1;
    *mtu = request.ifr_mtu;
    return 0;
}

int set_mtu(const char *ifname, int mtu)
{
    struct ifreq request;
    if (mtu < SAMPLE_MTU_MIN || mtu > SAMPLE_MTU_MAX) {
        errno = EINVAL;
        return -1;
    }
    if (prepare_request(&request, ifname) < 0)
        return -1;
    request.ifr_mtu = mtu;
    return call_ioctl(SIOCSIFMTU, &request);
}

prepare_request()は名前の長さを確認し、初期化した struct ifreq の ifr_name に名前を入れます。call_ioctl()はソケットを開いて要求を実行し、ソケットを閉じます。errnoを保存しているのは、失敗理由を呼び出し元で使うためです。ソケットを使いますが、この処理でネットワークへパケットを送信するわけではありません。

list_if()は SIOCGIFCONF を使い、必要な領域の大きさを調べてから一覧を取得します。取得中にアドレスが増えて領域が不足した場合はエラーとして返します。ここで得られるのはIPv4アドレスを持つインタフェースの情報です。

get_if_ipv4()は SIOCGIFADDR でIPv4アドレスを取得し、inet_ntop()で文字列に変換します。get_mtu()は SIOCGIFMTU の結果を ifr_mtu から読みます。set_mtu()は設定値を同じメンバーに入れ、SIOCSIFMTU で変更を要求します。変更には適切な権限が必要です。

例えば get_mtu() が呼ばれた場合、処理は次の順序で進みます。

  1. prepare_request()にインタフェース名を渡し、ifreqに設定します。
  2. call_ioctl()に SIOCGIFMTU と構造体を渡し、カーネルに現在値を問い合わせます。
  3. 成功したら request.ifr_mtu を呼び出し元の変数へ書き込みます。失敗した場合は -1 を返します。

MTUの変更も同様にインタフェース名を設定しますが、呼び出し前に request.ifr_mtu へ新しい値を入れます。 カーネルが変更を拒否した場合は成功扱いにせず、エラーを呼び出し元へ返します。

5.4つの操作をUBUSメソッドにする:ubus_server.c

files/src/bin/ubus_server.c を作成します。画面側から直接IOCTLを呼ぶのではなく、この常駐プログラムにUBUSを通じて処理を依頼します。

nano files/src/bin/ubus_server.c
/*
 * Copyright (C) 2024 utakamo <contact@utakamo.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libubus.h>
#include <libubox/blobmsg.h>
#include <libubox/uloop.h>
#include "ioctl_tool.h"

enum { ARG_IFNAME, ARG_MTU, ARG_COUNT };
static const struct blobmsg_policy interface_policy[] = {
    [ARG_IFNAME] = { .name = "ifname", .type = BLOBMSG_TYPE_STRING },
};
static const struct blobmsg_policy mtu_policy[] = {
    [ARG_IFNAME] = { .name = "ifname", .type = BLOBMSG_TYPE_STRING },
    [ARG_MTU] = { .name = "mtu", .type = BLOBMSG_TYPE_INT32 },
};
static struct blob_buf reply;

static int ioctl_error(const char *method)
{
    int error = errno;
    fprintf(stderr, "%s: %s\n", method, strerror(error));
    if (error == EPERM || error == EACCES)
        return UBUS_STATUS_PERMISSION_DENIED;
    if (error == ENODEV || error == ENXIO)
        return UBUS_STATUS_NOT_FOUND;
    if (error == EINVAL || error == EADDRNOTAVAIL)
        return UBUS_STATUS_INVALID_ARGUMENT;
    return UBUS_STATUS_UNKNOWN_ERROR;
}

static int list_interfaces(struct ubus_context *ctx, struct ubus_object *object,
                           struct ubus_request_data *req, const char *method,
                           struct blob_attr *message)
{
    (void)object;
    (void)message;
    struct ifreq *interfaces;
    size_t count;
    if (list_if(&interfaces, &count) < 0)
        return ioctl_error(method);

    blob_buf_init(&reply, 0);
    void *array = blobmsg_open_array(&reply, "interfaces");
    for (size_t i = 0; i < count; i++) {
        char address[INET_ADDRSTRLEN];
        const struct sockaddr_in *value = (const struct sockaddr_in *)&interfaces[i].ifr_addr;
        if (!inet_ntop(AF_INET, &value->sin_addr, address, sizeof(address))) {
            free(interfaces);
            return ioctl_error(method);
        }
        void *entry = blobmsg_open_table(&reply, NULL);
        blobmsg_add_string(&reply, "name", interfaces[i].ifr_name);
        blobmsg_add_string(&reply, "ipv4_addr", address);
        blobmsg_close_table(&reply, entry);
    }
    blobmsg_close_array(&reply, array);
    free(interfaces);
    return ubus_send_reply(ctx, req, reply.head);
}

static int interface_call(struct ubus_context *ctx, struct ubus_object *object,
                          struct ubus_request_data *req, const char *method,
                          struct blob_attr *message)
{
    (void)object;
    struct blob_attr *args[ARG_COUNT] = {0};
    if (!message)
        return UBUS_STATUS_INVALID_ARGUMENT;
    blobmsg_parse(mtu_policy, ARG_COUNT, args, blob_data(message), blob_len(message));
    if (!args[ARG_IFNAME])
        return UBUS_STATUS_INVALID_ARGUMENT;
    const char *ifname = blobmsg_get_string(args[ARG_IFNAME]);
    size_t length = strlen(ifname);
    if (!length || length >= IFNAMSIZ || blobmsg_data_len(args[ARG_IFNAME]) != length + 1)
        return UBUS_STATUS_INVALID_ARGUMENT;

    blob_buf_init(&reply, 0);
    blobmsg_add_string(&reply, "ifname", ifname);
    if (!strcmp(method, "get_if_ipv4")) {
        char address[INET_ADDRSTRLEN];
        if (get_if_ipv4(ifname, address, sizeof(address)) < 0)
            return ioctl_error(method);
        blobmsg_add_string(&reply, "ipv4_addr", address);
    } else {
        int mtu;
        if (!strcmp(method, "set_mtu")) {
            if (!args[ARG_MTU])
                return UBUS_STATUS_INVALID_ARGUMENT;
            uint32_t requested = blobmsg_get_u32(args[ARG_MTU]);
            if (requested < SAMPLE_MTU_MIN || requested > SAMPLE_MTU_MAX)
                return UBUS_STATUS_INVALID_ARGUMENT;
            if (set_mtu(ifname, (int)requested) < 0)
                return ioctl_error(method);
        }
        if (get_mtu(ifname, &mtu) < 0)
            return ioctl_error(method);
        blobmsg_add_u32(&reply, "mtu", mtu);
    }
    return ubus_send_reply(ctx, req, reply.head);
}

static const struct ubus_method methods[] = {
    UBUS_METHOD_NOARG("list_if", list_interfaces),
    UBUS_METHOD("get_if_ipv4", interface_call, interface_policy),
    UBUS_METHOD("get_mtu", interface_call, interface_policy),
    UBUS_METHOD("set_mtu", interface_call, mtu_policy),
};
static struct ubus_object_type object_type = UBUS_OBJECT_TYPE("sample03-ioctl", methods);
static struct ubus_object object = {
    .name = "ioctl-tool",
    .type = &object_type,
    .methods = methods,
    .n_methods = ARRAY_SIZE(methods),
};

static void connection_lost(struct ubus_context *ctx)
{
    (void)ctx;
    uloop_end(); /* procd respawns this process and it reconnects to ubusd. */
}

int main(int argc, char **argv)
{
    const char *socket_path = NULL;
    if (argc == 3 && !strcmp(argv[1], "-s"))
        socket_path = argv[2];
    else if (argc != 1) {
        fprintf(stderr, "Usage: %s [-s ubus-socket]\n", argv[0]);
        return EXIT_FAILURE;
    }
    if (uloop_init() < 0)
        return EXIT_FAILURE;
    struct ubus_context *ctx = ubus_connect(socket_path);
    if (!ctx) {
        fprintf(stderr, "Failed to connect to ubus\n");
        uloop_done();
        return EXIT_FAILURE;
    }
    ctx->connection_lost = connection_lost;
    ubus_add_uloop(ctx);
    int status = ubus_add_object(ctx, &object);
    if (status)
        fprintf(stderr, "Failed to register ioctl-tool: %s\n", ubus_strerror(status));
    else
        uloop_run();
    blob_buf_free(&reply);
    ubus_free(ctx);
    uloop_done();
    return status ? EXIT_FAILURE : EXIT_SUCCESS;
}

methodsでは、list_if、get_if_ipv4、get_mtu、set_mtuを登録しています。objectの名前は ioctl-tool です。interface_policyとmtu_policyは、受け取る引数名と型を定義します。

list_interfaces()は一覧を取得し、interfaces配列に名前とIPv4アドレスを入れて返します。interface_call()は名前とMTUの入力を確認し、指定されたメソッドに応じて処理を選びます。set_mtuの成功後は、get_mtu()で現在値を読み直して応答に入れます。

main()は uloopを初期化してUBUSに接続し、オブジェクトを登録してから要求を待ちます。IOCTLでエラーが発生した場合は、ioctl_error()が原因を標準エラー出力に記録し、UBUSのエラーを返します。

例えば set_mtu の要求を受け取ってから応答するまでの流れは次の通りです。

  1. interface_call()が blobmsg_parse() で引数を取り出し、ifname と mtu を確認します。
  2. set_mtu()でMTUの変更を要求し、成功したら get_mtu() で現在値を読み直します。
  3. ifnameとmtuを応答用データへ追加し、ubus_send_reply()で呼び出し元へ返します。

list_ifは引数なしで受け付けるため、UBUS_METHOD_NOARGで登録します。 その他の3つはインタフェース名を受け取り、set_mtuだけは変更する値も必要です。 C側でも引数を検証しているので、LuCIを通さず ubus call で直接呼び出す場合にも不正な入力を拒否します。

6.自動起動用のサービスを作る

OpenWrtのサービス管理機構であるprocdに、Cプログラムを起動してもらいます。files/etc/init.d/luci-app-sample03 を作成してください。

nano files/etc/init.d/luci-app-sample03
#!/bin/sh /etc/rc.common

USE_PROCD=1
START=95
STOP=10

start_service() {
    procd_open_instance
    procd_set_param command /usr/bin/luci-app-sample03
    procd_set_param respawn
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_close_instance
}

commandはパッケージのインストール先にある実行ファイルを指定します。respawnによりプロセス終了後の再起動をprocdに任せ、stdout・stderrをシステムログに出します。パッケージ作成時に実行権限を付けるので、作業ファイルに個別のchmodは不要です。

LuCIのコントローラーと画面を作る

1.LuCIコントローラーを作成する:module.lua

続いて、メニューとAPIを登録するLuaファイルを作ります。作業ディレクトリは引き続き ~/src/luci-app-sample03 です。files/luci/controller/module.lua に次の全文を保存します。

nano files/luci/controller/module.lua
module("luci.controller.luci-app-sample03.module", package.seeall)

local http = require "luci.http"
local jsonc = require "luci.jsonc"
local ubus = require "ubus"
local allowed = { list_if = true, get_if_ipv4 = true, get_mtu = true, set_mtu = true }

function index()
    entry({"admin", "status", "sample03"}, firstchild(), _("IOCTL Sample"), 30).dependent = false
    entry({"admin", "status", "sample03", "ioctl"},
        template("luci-app-sample03/ioctl"), _("IOCTL Tool"), 10).dependent = false
    entry({"admin", "status", "sample03", "api"}, post("action_ioctl_call")).leaf = true
end

local function respond(status, payload)
    http.status(status)
    http.prepare_content("application/json")
    http.write_json(payload)
end

function action_ioctl_call()
    -- post() checks the HTTP method and LuCI session token before calling us.
    local encoded = http.formvalue("payload")
    local parsed, payload = pcall(jsonc.parse, type(encoded) == "string" and encoded or "")
    if not parsed or type(payload) ~= "table" or type(payload.method) ~= "string"
        or not allowed[payload.method] or type(payload.args) ~= "table" then
        respond(400, { ok = false, message = "Invalid method or arguments." })
        return
    end

    local method, args = payload.method, {}
    if method ~= "list_if" then
        local name = payload.args.ifname
        if type(name) ~= "string" or #name == 0 or #name >= 16 or name:find("%z") then
            respond(400, { ok = false, message = "Interface name must be 1 to 15 bytes." })
            return
        end
        args.ifname = name
    end
    if method == "set_mtu" then
        local mtu = payload.args.mtu
        if type(mtu) ~= "number" or mtu ~= mtu or mtu < 68 or mtu > 65536 or mtu % 1 ~= 0 then
            respond(400, { ok = false, message = "MTU must be an integer from 68 to 65536." })
            return
        end
        args.mtu = mtu
    end

    local conn = ubus.connect()
    if not conn then
        respond(502, { ok = false, message = "Failed to connect to ubus." })
        return
    end
    local ok, result, err = pcall(conn.call, conn, "ioctl-tool", method, args)
    conn:close()
    if not ok or type(result) ~= "table" then
        respond(502, { ok = false, message = "ubus call failed (" .. tostring(err or result)
            .. "). Check the interface, service and system log." })
        return
    end
    respond(200, { ok = true, result = result })
end

index()は Status → IOCTL Sample → IOCTL Tool の画面と、画面が呼び出すAPIを登録します。post("action_ioctl_call")はPOSTとLuCIのセッショントークンを確認してから関数を呼び出します。

action_ioctl_call()はフォーム項目 payload をJSONとして解析し、許可した4つのメソッド、インタフェース名、MTUを検証します。その後 ubus.connect()で接続して ioctl-tool を呼び出し、成功時は結果をJSONで返します。入力エラーはHTTP 400、UBUSへの接続や呼び出しの失敗はHTTP 502として返します。

画面のURLは /cgi-bin/luci/admin/status/sample03/ioctl、APIのパスは /cgi-bin/luci/admin/status/sample03/api です。 allowedにあるメソッドだけを受け付け、list_if以外ではインタフェース名を必須にします。 set_mtuの場合は、MTUが68〜65536の整数であることも確認します。

2.操作画面を作成する:ioctl.htm

次に、ボタンと入力欄、JavaScriptを含むLuCIテンプレートを作成します。files/luci/view/luci-app-sample03/ioctl.htm に次の全文を保存してください。先頭と末尾のLuCI用タグも含めてコピーします。

nano files/luci/view/luci-app-sample03/ioctl.htm
<%+header%>
<h2><%:IOCTL Tool%></h2>

<div class="cbi-section">
    <h3>IPv4 Interface List</h3>
    <p>Interfaces with an IPv4 address. Click a name to select it.</p>
    <button type="button" class="cbi-button cbi-button-action" id="refresh-list">Refresh</button>
    <table class="table" id="iface-table">
        <thead><tr><th>Interface</th><th>IPv4 Address</th></tr></thead>
        <tbody><tr><td colspan="2">Press Refresh to load.</td></tr></tbody>
    </table>
</div>

<div class="cbi-section">
    <h3>Interface Details</h3>
    <p>You can also enter an interface name that is absent from the IPv4 list.</p>
    <label for="ifname">Interface</label>
    <input id="ifname" type="text" maxlength="15" placeholder="eth0">
    <button type="button" class="cbi-button cbi-button-action" id="get-ipv4">Get IPv4</button>
    <button type="button" class="cbi-button cbi-button-action" id="get-mtu">Get MTU</button>
</div>

<div class="cbi-section">
    <h3>MTU Settings</h3>
    <p>Applies to the interface selected above. Use a test interface and record its original MTU.
        This changes the running interface; it does not save the network configuration.</p>
    <label for="mtu">MTU</label>
    <input id="mtu" type="number" min="68" max="65536" step="1" placeholder="1500">
    <button type="button" class="cbi-button cbi-button-apply" id="set-mtu">Set MTU</button>
</div>

<div class="cbi-section">
    <h3>Action Result</h3>
    <pre id="action-result" role="status" aria-live="polite">Ready.</pre>
</div>
<style>
    #action-result { padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
    #action-result.error { color: #b42318; }
    #iface-table button { font: inherit; }
</style>
<script>
(function() {
    'use strict';
    var apiUrl = '<%=luci.dispatcher.build_url("admin", "status", "sample03", "api")%>';
    var resultBox = document.getElementById('action-result');
    var nameInput = document.getElementById('ifname');
    var mtuInput = document.getElementById('mtu');
    var busy = false;

    function show(value, error) {
        resultBox.className = error ? 'error' : '';
        resultBox.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
    }

    function ubusCall(method, args) {
        return new Promise(function(resolve, reject) {
            XHR.post(apiUrl, { payload: JSON.stringify({ method: method, args: args }) }, function(xhr, json) {
                if (xhr.status >= 200 && xhr.status < 300 && json && json.ok)
                    resolve(json.result);
                else
                    reject(new Error(json && json.message ? json.message : 'HTTP ' + xhr.status));
            }).catch(reject);
        });
    }

    function run(action) {
        if (busy) return;
        busy = true;
        document.querySelectorAll('.cbi-section button').forEach(function(button) { button.disabled = true; });
        show('Loading...');
        Promise.resolve().then(action).catch(function(error) {
            show(error.message, true);
        }).then(function() {
            busy = false;
            document.querySelectorAll('.cbi-section button').forEach(function(button) { button.disabled = false; });
        });
    }

    function interfaceName() {
        var name = nameInput.value.trim();
        if (!name) throw new Error('Select or enter an interface name.');
        return name;
    }

    function refresh() {
        run(function() {
            return ubusCall('list_if', {}).then(function(result) {
                var body = document.querySelector('#iface-table tbody');
                body.textContent = '';
                (result.interfaces || []).forEach(function(item) {
                    var row = body.insertRow();
                    var button = document.createElement('button');
                    button.type = 'button';
                    button.textContent = item.name;
                    button.disabled = true;
                    button.addEventListener('click', function() { nameInput.value = item.name; });
                    row.insertCell().appendChild(button);
                    row.insertCell().textContent = item.ipv4_addr;
                });
                if (!body.rows.length) {
                    var cell = body.insertRow().insertCell();
                    cell.colSpan = 2;
                    cell.textContent = 'No IPv4 interfaces.';
                }
                show(result);
            });
        });
    }

    document.getElementById('refresh-list').addEventListener('click', refresh);
    ['get_if_ipv4', 'get_mtu', 'set_mtu'].forEach(function(method, index) {
        document.getElementById(['get-ipv4', 'get-mtu', 'set-mtu'][index]).addEventListener('click', function() {
            run(function() {
                var args = { ifname: interfaceName() };
                if (method === 'set_mtu') {
                    if (!mtuInput.value || !mtuInput.checkValidity())
                        throw new Error('MTU must be an integer from 68 to 65536.');
                    args.mtu = Number(mtuInput.value);
                }
                return ubusCall(method, args).then(function(result) {
                    if (result.mtu != null && nameInput.value.trim() === args.ifname)
                        mtuInput.value = result.mtu;
                    show(result);
                });
            });
        });
    });
    window.addEventListener('load', refresh);
})();
</script>
<%+footer%>

ubusCall()は、メソッド名と引数をJSON文字列にし、payloadというフォーム項目として XHR.post() で送ります。LuCIのトークンも送信されます。リクエスト全体をJSON本文にする方式ではありません。Lua側の http.formvalue("payload") がこの項目を受け取ります。

refresh()はIPv4一覧を表示し、名前を選ぶと入力欄に反映します。Get IPv4、Get MTU、Set MTUはそれぞれ対応するメソッドを呼び出します。取得結果やエラーは Action Result に表示します。処理中はボタンを無効にして重複操作を防ぎ、表示には textContent を使います。

画面の各操作とUBUSメソッドの対応は次の通りです。

  • Refresh:list_ifを呼び出してIPv4アドレスを持つインタフェースを一覧表示します。
  • Get IPv4:入力された名前を get_if_ipv4 に渡します。
  • Get MTU:get_mtuの結果を表示し、MTU入力欄にも反映します。
  • Set MTU:名前と入力値を set_mtu に渡し、変更後の値を表示します。

パッケージをビルドして起動する

作成したソースを開発用PCのbuildrootでパッケージにし、OpenWrt機器へ転送してインストールします。 はじめに、全体の流れを図で確認します。

開発用PCでソースをローカルfeedに登録し、buildrootでビルドしたIPKをOpenWrt機器へ転送してインストールする流れ

1.Cプログラム用のmakefileを作成する

まず、~/src/luci-app-sample03 内の files/src/bin/makefile を作成します。 2つのCファイルをコンパイルし、1つの実行ファイルにするための設定です。

nano files/src/bin/makefile
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra
LDLIBS += -lubus -lubox
OBJ := ubus_server.o ioctl_tool.o

all: luci-app-sample03

%.o: %.c ioctl_tool.h
	$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

luci-app-sample03: $(OBJ)
	$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)

clean:
	rm -f luci-app-sample03 $(OBJ)

.PHONY: all clean

リンクするライブラリは libubus と libubox です。 %.oの規則でCファイルをコンパイルし、最後に実行ファイルを作ります。 コマンド行の先頭はスペースではなくタブです。

2.パッケージ用のMakefileを作成する

続いて、作業ディレクトリ直下に大文字の Makefile を作ります。 buildrootのクロスコンパイラとフラグを先ほどのmakefileに渡し、できたバイナリやLuaファイルをパッケージ内へ配置します。

nano Makefile
include $(TOPDIR)/rules.mk

PKG_NAME:=luci-app-sample03
PKG_VERSION:=1.0
PKG_RELEASE:=2
PKG_LICENSE:=LGPL-2.1

include $(INCLUDE_DIR)/package.mk

define Package/luci-app-sample03
    SECTION:=utakamo
    CATEGORY:=utakamo
    TITLE:=LuCI IOCTL sample
    DEPENDS:=+luci-compat +libubus-lua +libubus +libubox
endef

define Build/Prepare
	mkdir -p $(PKG_BUILD_DIR)
	$(CP) ./files/src/bin/* $(PKG_BUILD_DIR)/
endef

define Build/Compile
	$(MAKE) -C $(PKG_BUILD_DIR) \
		CC="$(TARGET_CC)" \
		CPPFLAGS="$(TARGET_CPPFLAGS)" \
		CFLAGS="$(TARGET_CFLAGS) -Wall -Wextra" \
		LDFLAGS="$(TARGET_LDFLAGS)"
endef

define Package/luci-app-sample03/install
	$(INSTALL_DIR) $(1)/etc/init.d $(1)/usr/bin
	$(INSTALL_BIN) ./files/etc/init.d/luci-app-sample03 $(1)/etc/init.d/
	$(INSTALL_BIN) $(PKG_BUILD_DIR)/luci-app-sample03 $(1)/usr/bin/
	$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller/luci-app-sample03
	$(INSTALL_DATA) ./files/luci/controller/module.lua $(1)/usr/lib/lua/luci/controller/luci-app-sample03/
	$(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/luci-app-sample03
	$(INSTALL_DATA) ./files/luci/view/luci-app-sample03/ioctl.htm $(1)/usr/lib/lua/luci/view/luci-app-sample03/
endef

$(eval $(call BuildPackage,luci-app-sample03))

luci-compatはLua形式のLuCIページに必要な互換ライブラリで、luci-lua-runtimeに依存します。 libubus-luaはLuaの require "ubus" に必要です。 Cのバイナリを含むため PKGARCH:=all は指定しません。 Makefileのレシピ行の先頭はタブです。配布アーカイブのファイルをそのまま使えば、字下げも保持できます。

3.buildrootにローカルfeedを追加する

次は開発用PCのbuildrootディレクトリで作業します。以下ではその場所を ~/openwrt とします。実際の配置先に読み替えて移動してください。 buildrootはインストール先と同じOpenWrt 24.10.0のリリース・ターゲットを用意します。準備方法はOpenWrtのbuildroot構築記事を参照してください。 標準feedを保持するため、feeds.confがまだなければ feeds.conf.default をコピーしてから編集します。 既に feeds.conf がある場合はコピーせず、そのファイルを編集してください。

kamo@pc:~$ cd ~/openwrt
kamo@pc:~/openwrt$ test -f feeds.conf || cp feeds.conf.default feeds.conf
kamo@pc:~/openwrt$ nano feeds.conf

次の1行は、作成した luci-app-sample03 の親ディレクトリをfeedとして登録する例です。 /home/kamo/src は例なので、実際の ~/src の絶対パスに置き換えてください。 別の端末で cd ~/src && pwd を実行すると確認できます。feeds.confには ~ や $HOME をそのまま書かず、表示された絶対パスを書きます。 既に utakamo feedがある場合は登録先を確認し、同名の行を重複させないでください。

src-link utakamo /home/kamo/src

feedを更新し、パッケージを登録します。

kamo@pc:~/openwrt$ ./scripts/feeds update -a
kamo@pc:~/openwrt$ ./scripts/feeds install -a

updateでfeedの情報を更新し、installでbuildrootからパッケージを選択できるようにします。 更新時には標準feedの取得や通信が発生します。エラーが出た場合は、その内容を確認して解消してから先へ進みます。

4.対象機器を選び、パッケージをビルドする

同じbuildrootディレクトリで設定画面を開きます。

kamo@pc:~/openwrt$ make menuconfig

対象機器のターゲット設定を維持したまま、utakamo → luci-app-sample03 を <*> で選択して保存します。 この設定でファームウェアイメージ全体をビルドすると、プラグインもイメージに組み込まれます。

menuconfigのutakamoカテゴリでluci-app-sample03が選択されている画面

まだ一度もbuildroot全体をビルドしておらず、ツールチェーンや依存パッケージが準備できていない場合は、先に次を実行します。 初回は必要なソースのダウンロードとビルドに時間や空き容量が必要です。実行する並列数は開発用PCに合わせて調整してください。

kamo@pc:~/openwrt$ make -j2

準備済みのbuildrootでは、次のコマンドで対象パッケージをビルドします。 ビルド済みの旧版がある場合にも対応するため、対象パッケージをcleanしてからコンパイルします。

kamo@pc:~/openwrt$ make package/luci-app-sample03/clean
kamo@pc:~/openwrt$ make package/luci-app-sample03/compile V=s

完了したら find bin/packages -name 'luci-app-sample03_*.ipk' で生成物の場所を確認します。例えば aarch64_cortex-a53 向けなら、bin/packages/aarch64_cortex-a53/utakamo/ に luci-app-sample03_1.0-2_aarch64_cortex-a53.ipk が生成されます。 ファイル名と配置先は、使用したターゲット・buildrootで確認してください。

5.転送・インストール・起動

ここでは、既に動作しているOpenWrt機器に、生成したパッケージをインストールします。 以下は先ほどの aarch64_cortex-a53 向けパッケージを、IPアドレス 192.168.1.1 の機器へ転送する例です。 ファイル名・アーキテクチャ・接続先は、自分の環境と生成結果に合わせて置き換えてください。

プラグインを組み込んでビルドしたファームウェアを既に機器へ導入している場合は、パッケージの転送とインストールは不要です。 OpenWrtのコンソールにログインし、サービスの起動とUBUSオブジェクトの確認に進んでください。

kamo@pc:~/openwrt$ scp bin/packages/aarch64_cortex-a53/utakamo/luci-app-sample03_1.0-2_aarch64_cortex-a53.ipk root@192.168.1.1:/tmp/
kamo@pc:~/openwrt$ ssh root@192.168.1.1

転送時にSFTPサーバーが見つからないエラーが出る場合は、旧SCP方式を指定する scp -O を使います。 ログイン後、パッケージを転送した場合はOpenWrt側で次を実行します。

root@OpenWrt:~# opkg update
root@OpenWrt:~# opkg install /tmp/luci-app-sample03_1.0-2_aarch64_cortex-a53.ipk

インストールが終わったら、サービスの自動起動を有効にして起動し、UBUSへの登録を確認します。 ファームウェアにプラグインを組み込んだ場合も、ここから実行してください。

root@OpenWrt:~# /etc/init.d/luci-app-sample03 enable
root@OpenWrt:~# /etc/init.d/luci-app-sample03 restart
root@OpenWrt:~# ubus -v list ioctl-tool

list_if、get_if_ipv4、get_mtu、set_mtuの4つが表示されれば、UBUS側の登録まで進んでいます。 LuCIへ入り直してもメニューが出ない場合は、次のコマンドでLuCIのキャッシュを削除し、Webサーバーを再起動します。

root@OpenWrt:~# rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache/*
root@OpenWrt:~# /etc/init.d/uhttpd restart

コンソールとLuCIで動作を確認する

1.一覧とIPv4を取得する

root@OpenWrt:~# ubus call ioctl-tool list_if '{}'

以下は応答の形式例です。インタフェース名・件数・アドレスは機器によって異なります。

{
    "interfaces": [
        { "name": "lo", "ipv4_addr": "127.0.0.1" },
        { "name": "br-lan", "ipv4_addr": "192.168.1.1" }
    ]
}
root@OpenWrt:~# ubus call ioctl-tool get_if_ipv4 '{"ifname":"br-lan"}'
root@OpenWrt:~# ubus call ioctl-tool get_mtu '{"ifname":"br-lan"}' 

2つの取得結果には、それぞれ ipv4_addr と mtu が含まれます。 一覧に eth0 が出なくても、IPv4がブリッジの br-lan に設定されている構成なら不自然ではありません。

例えば br-lan のIPv4アドレスが 192.168.1.1、MTUが 1500 の場合、2つの応答はそれぞれ次の形式になります。

{ "ifname": "br-lan", "ipv4_addr": "192.168.1.1" }
{ "ifname": "br-lan", "mtu": 1500 }

これは表示形式の例です。実際のインタフェース名と値は、使用している機器に合わせて読み替えてください。

2.検証用インタフェースのMTUを変更して戻す

MTU変更は実際のインタフェースへ即時に反映されます。SSHやLuCIの通信に使っているLANへ適用すると接続に影響するため、 接続経路から独立した検証用インタフェースで試してください。 以下では、その名前を test0 としています。名前は事前に用意したインタフェースへ置き換えます。

検証用インタフェースを作る場合

独立した検証用インタフェースがない場合は、物理的なLANにつながらないdummyインタフェースを利用できます。 OpenWrt 24.10.0でdummyドライバーと完全版の ip コマンドが未導入なら、対応するパッケージをインストールします。 パッケージの取得には機器のネットワーク接続と、その機器のリリース・カーネルに合うパッケージリポジトリが必要です。

root@OpenWrt:~# opkg update
root@OpenWrt:~# opkg install ip-full kmod-dummy

既に test0 が使われていないことを ip link show dev test0 で確認してから、次を実行します。 同名のインタフェースが存在する場合は別の未使用名を選び、以降のコマンドと画面の入力もその名前に置き換えてください。

root@OpenWrt:~# ip link add dev test0 type dummy
root@OpenWrt:~# ip link set dev test0 up
root@OpenWrt:~# ip link show dev test0

作成した直後の test0 にはIPv4アドレスを設定していないため、list_ifの一覧には表示されません。 MTUの取得・変更は名前を直接指定して試せます。作成に失敗した場合は、dummyドライバーやパッケージの対応状況を確認してください。

root@OpenWrt:~# ubus call ioctl-tool get_mtu '{"ifname":"test0"}'
root@OpenWrt:~# ubus call ioctl-tool set_mtu '{"ifname":"test0","mtu":1400}'
root@OpenWrt:~# ubus call ioctl-tool get_mtu '{"ifname":"test0"}' 

最初の取得値を控え、変更後の取得値が1400になっていることを確認します。 テスト後は元の値へ戻します。元のMTUが1500だった場合の復元コマンドは次の通りです。

root@OpenWrt:~# ubus call ioctl-tool set_mtu '{"ifname":"test0","mtu":1500}' 

このサンプルは稼働中のインタフェースを直接操作し、UCIのネットワーク設定を保存しません。 再起動やネットワーク設定の再適用後も値を維持するための機能ではありません。

3.同じ操作をLuCIで行う

LuCIへログインし、Status → IOCTL Sample → IOCTL Tool を開きます。 ページのパスは /cgi-bin/luci/admin/status/sample03/ioctl です。

Status のメニューに IOCTL Sample が追加されます。

LuCIのStatusメニューにIOCTL Sampleが表示されている画面

ページを開くと、IPv4アドレスを持つインタフェースの一覧と、IPv4・MTUの操作欄が表示されます。Refreshで一覧を更新できます。

LuCIのIOCTL ToolでIPv4一覧とインタフェース・MTUの操作欄を表示した画面

一覧からインタフェース名を選ぶか、Interface 欄に入力し、Get IPv4 を押すとIPv4アドレスを取得できます。 次の画面は br-lan の結果が Action Result に表示された例です。

br-lanのIPv4アドレス取得結果がAction Resultに表示された画面

Get MTU は現在のMTUを取得し、入力欄と Action Result に表示します。 Set MTU では、入力した値への変更を要求し、変更後の値を表示します。 次の画面は br-lan のMTUとして 1000 が表示された例です。表示される値は環境や操作内容によって異なります。

br-lanのMTUとして1000が入力欄とAction Resultに表示された画面

MTUの変更を試すときは、画像中の名前や値をそのまま使わず、接続経路から独立した検証用インタフェースを指定してください。 あらかじめ現在値を控え、変更後は Get MTU で再取得して、元の値へ戻します。

UBUSのコンソール操作が成功して画面操作だけ失敗する場合は、ブラウザの開発者ツールでAPIへのPOSTを見ます。 フォームに payload と token が入り、応答JSONの ok がtrueになっているかが確認点です。

この手順で一時的にdummyインタフェースを作成した場合は、コンソールとLuCIでの確認が終わった後に、次のコマンドで削除できます。 別の名前で作成した場合は、その名前に置き換えてください。既存のインタフェースは削除しないでください。

root@OpenWrt:~# ip link delete dev test0

うまく動かないときの切り分け

症状確認する箇所
ioctl-toolが見つからないサービスを再起動し、logread -e luci-app-sample03でUBUS接続・登録エラーを見る
インタフェースが一覧に出ないその名前にIPv4が割り当てられているかを見る。MTU取得は名前の直接指定も可能
MTU変更が失敗する対象名、整数値、対応するMTU範囲を確認し、ログのIOCTLエラーを読む
APIがHTTP 403を返すLuCIへログインし直して画面を再読み込みし、トークンを更新する
APIがHTTP 400を返すメソッド名、payloadのJSON、引数の型と範囲を見る
APIがHTTP 502を返す同じメソッドを ubus call で試し、サービスとシステムログを確認する
依存関係・アーキテクチャのエラーでインストールできない機器とbuildrootのリリース・ターゲットを揃え、対応feedの依存パッケージを用意する

おわりに

今回は、4つのIOCTL操作を題材に、C言語のUBUSサーバーとLuCIを連携させました。 C側で機器の処理を実装し、UBUSのメソッドとして公開し、Luaコントローラーを経由して画面から呼び出す構成です。

新しい処理を追加する際も、まずコンソールの ubus call で引数と応答を確かめてから画面へつなぐと、問題のある箇所を追いやすくなります。 独自機能を持つLuCIプラグインを作るときの土台として活用してみてください。

参考文献