読者です 読者をやめる 読者になる 読者になる

ISC DHCP serverでIPアドレスのリース時に外部コマンドを実行する

ISC DHCP serverDHCPサーバを運用するとき,IPアドレスをリースするタイミングで任意のコマンドを実行したいことがある.例えば,クライアントのMACアドレスとリースしたIPアドレスのペアをデータベースに入れておきたい,といった場合.

こういった場合,on commit/on release/on expiryが使える.dhcpd.confに次のような設定を書いておくと,それぞれのタイミングで外部のコマンドが実行される.

on commit {
    execute("/path/to/script", "arg1", "arg2", "arg3");
}

on release {
    execute("/path/to/script", "arg1", "arg2", "arg3");
}

on expiry {
    execute("/path/to/script", "arg1", "arg2", "arg3");
}

各コマンドはそれぞれ次のタイミングで実行される:

  • on commit
    • サーバがクライアントにIPアドレスをリースしたタイミング
  • on release
    • クライアントがIPアドレスをリリースしたタイミング
  • on expiry
    • サーバがクライアントにリースしたIPアドレスのリース期限が切れたタイミング

実行する外部コマンドにIPアドレスMACアドレスを渡す

クライアントのMACアドレスおよびIPアドレスは,それぞれhardware変数およびleased-address変数に格納されている.ただしこれらの変数の値は整数で表現されているので,通常そのままでは扱い辛い.そこで文字列に変換した上で外部コマンドに渡す.

on commit {
    set clip = binary-to-ascii(10, 8, ".", leased-address);
    set clhw = concat (
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
    );
    execute("/path/to/script", clip, clhw);
}

たまにMACアドレスの取得方法として次のような方法を紹介している記事がある.この方法だと"02:01:23:0a:bc:de"が"2:1:23:a:bc:de"と変換されてしまう.

set clhw = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));

外部コマンド側の実装

on commit時にIPアドレスMACアドレスのペアをRedisに突っ込み,on expiryで対応するエントリを削除するスクリプトだと,下のような感じになる.

#!/usr/bin/env python
"""Manage bindings between an IP address and a MAC address"""

import redis

import sys
import argparse

kvs = redis.Redis(host="127.0.0.1", port=6379, db=1)


def on_commit(ip_address, mac_address):
    """Insert a newly-leased IP address and its corresponding MAC address to the redis server"""
    kvs.set(ip_address, mac_address)

    return


def on_expiry(ip_address):
    """Remove the expired IP address from the redis server"""
    kvs.delete(ip_address)

    return


def main():
    """Parse command-line arguments and call a corresponding function"""
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers()

    subparser_commit = subparsers.add_parser("commit")
    subparser_commit.set_defaults(
        func=lambda command_args: on_commit(command_args.ip_address, command_args.mac_address))
    subparser_commit.add_argument("ip_address")
    subparser_commit.add_argument("mac_address")

    subparser_expiry = subparsers.add_parser("expiry")
    subparser_expiry.set_defaults(
        func=lambda command_args: on_expiry(command_args.ip_address))
    subparser_expiry.add_argument("ip_address")

    args = parser.parse_args()

    try:
        subcommand_func = args.func
    except AttributeError:
        parser.print_usage()
        sys.exit(2)

    subcommand_func(args)

if __name__ == '__main__':
    main()

以前,この仕組みを使ってCaptive Portalのようなものを書いて運用していた.WebページにアクセスしてきたクライアントのIPアドレスからMACアドレスが引けるようになるので,アカウント(人)とMACアドレス(デバイス)の紐付けが簡単に実現できる.

参考