dhcp だけど自分のホストくらいは名前で参照したくならない?

dhcp かつ Dymanic DNS を使わないことが前提で。


DNS の接続時に自分を登録するのが Dynamic DNS なんだったかな。
Windows のネットワーク接続設定にあるやつ。
でも、普通の LAN ではやってない、と思う。


となると、可能なのはこの 2 つか。

  1. RESOLVER を騙す
  2. /etc/hosts を書き換える


RESOLVER を騙すのはかなりオオゴトな気がします。
libresolv ってくらいですし。
なので却下。
普通に /etc/hosts を書き換えることにしますか。


ネットワークインターフェースが有効になったときのためのフックスクリプトは、最近では /etc/network の下に置いてあるみたい。
Mac は知らない。Automator で好きにすればいいと思う。
/etc/network/if-up.d の下を見ると、いくつかスクリプトが入ってる。

$ ls /etc/network/if-up.d
mountnfs  ntpdate  openssh-server  wpasupplicant

中身を見てみると、だいたいこれらが呼ばれるときに渡される引数が分かります。
引数は環境変数経由で渡されるのが常識みたいです。
よく分からないときは適当にファイルに落としてみればいいと思います。
こんなかんじで。

$ cat /etc/network/if-up.d/test
#!/bin/bash
# すべての出力を /var/tmp/hoge にリダイレクト
exec 2>&1
exec 1>>/var/tmp/hoge
# 環境変数をすべて標準出力に
/usr/bin/env
$ chmod +x /etc/network/if-up.d/test
$ sudo /etc/init.d/networking restart
$ cat /var/tmp/hoge
MODE=start
VERBOSITY=0
PHASE=post-up
IF_METRIC=100
ADDRFAM=inet
METHOD=dhcp
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/etc/network/if-up.d
SHLVL=1
IFACE=eth1
LOGICAL=eth1
_=/usr/bin/env

見て分かるとおり、/etc/hosts に記録するために必要な IP アドレスがありません。
なので、ここのフックポイントは使えないようです。


初心に帰って dhclient-script とか使うことになるんでしょうか。
以前 dhcp クライアントはいろんなものがあった気がするのですが、
最近は Internet Systems Consortium DHCP Client V3.1.1 がデフォルトみたいです。

$ ls /etc/dhcp3/
dhclient-enter-hooks.d	dhclient-exit-hooks.d  dhclient.conf

名前からして、dhclient-enter-hooks.d が DHCP の問い合わせをした時に参照されそう。
ここにはすでに debug スクリプトがいたので、それを実行して引数とかを確認できたので細かいことは省略。


ところで、このフックスクリプトはどういうタイミングで呼ばれるんでしょう。
特に dhcp にしているネットワークインターフェースが複数あるときとか。


特に調べなかったのですが、インターフェースごとにばらばらに適当に呼ばれてる感じです。
そもそも /etc/init.d/networking が悪いような気もしますが。


ということで、きちんと /etc/hosts を更新するには、ファイルの排他処理をしないといけません。
しかも、フックスクリプトの呼び出しは DHCP の問い合わせをする時だけなので、
google:シェルスクリプト 排他処理とか検索結果の上位を占める多重起動回避とは違ったことをしないといけません。
javaruby の synchronized が必要ですが、シェルスクリプトでそんなんを作るのはあまり楽しそうじゃありません。
よってスクリプト自体を ruby で書くのがよさそうです。

スクリプトは書き途中だけど、別にいい方法が見つかったらそうするつもり。


結局、こんなかんじになりました。
/etc/dhcp3/dhclient-enter-hooks.d の下に置くものは sh スクリプトじゃないと駄目みたいなので、
そちらは sh です。

#!/usr/bin/ruby

require 'logger'

class App
    LOCK_DIR = "/tmp/" + File.basename(__FILE__) + ".lock"
    ETC_HOSTS = "/etc/hosts"

    def initialize
        @log = Logger.new("/var/tmp/#{File.basename($0)}.log")
        @log.progname = File.basename($0)
        @log.level = Logger::DEBUG
        @log.debug("args dump")
    end

def update_etc_hosts
    ENV.each{|k,v| @log.debug("#{k}=#{v}") }
    lock_dir = nil
    begin
        @log.info("enter critical area")
        if File.exists?(LOCK_DIR)
          while ! File.exists?(LOCK_DIR)
            log.info("another process exists, wait for 1 second")
            sleep(1000)
          end
        end
        lock_dir = Dir.mkdir(LOCK_DIR)
        entries = Array.new
        open(ETC_HOSTS, "r") {|etc_hosts|
            entries = etc_hosts.readlines
        }
        new_etc_hosts = 
        begin
            @log.info("reason: #{ENV["reason"]}")
            case ENV["reason"]
            when "RELEASE"
                proc_reason_release entries
            when "BOUND"
                proc_reason_bound entries
            else
                @log.info("nothing to do")
            end
        rescue => e
            @log.error(e)
        ensure
            new_etc_hosts.close if new_etc_hosts
        end
    
        if ENV["reason"] =~ /RELEASE|BOUND/
            @log.info("diff")
            open("| /usr/bin/diff #{ETC_HOSTS} /var/tmp/#{Process.pid}_etc_hosts", "r") {|diff|
                while s = diff.gets
                    @log.info(s)
                end
            }
            File.rename("/var/tmp/#{Process.pid}_etc_hosts", ETC_HOSTS)
        end
    ensure
        @log.info("leave critical area")
        if lock_dir != nil and File.exists?(LOCK_DIR)
            Dir.delete(LOCK_DIR)
        end
    end
end

def proc_reason_release(entries)
    if ENV.key?("old_ip_address") and ENV["old_ip_address"] != "127.0.0.1"
        @log.info("comment out address: #{ENV["old_ip_address"]}")
        open("/var/tmp/#{Process.pid}_etc_hosts", "w") {|new_etc_hosts|
            entries.each {|line|
                s = line.gsub(/^#{ENV["old_ip_address"]}/, "#" + ENV["old_ip_address"])
                new_etc_hosts.puts(s)
            }
        }
    end
end

def proc_reason_bound(entries)
    if ENV.key?("new_ip_address") and ENV["new_ip_address"] != "127.0.0.1"
    open("/var/tmp/#{Process.pid}_etc_hosts", "w") {|new_etc_hosts|
        entries.map {|line| new_etc_hosts.puts(line) }
        @log.info("add new address: #{ENV["new_ip_address"]}")
        open("/etc/hostname", "r") { |etc_hostname|
            hostname = etc_hostname.gets.chomp
            new_etc_hosts.puts("#{ENV["new_ip_address"]} #{hostname}")
        }
    }
    end
end
end

if $0 == __FILE__
  App.new.update_etc_hosts
end