<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[朝日薫 - てくにかるむ]]></title><description><![CDATA[「エラーをなくすことは非常に有益で時には新しい真実や事実を作り上げるよりも勝る」
ー チャールズ・ダーウィン]]></description><link>http://multix.jp/</link><generator>Ghost 0.6</generator><lastBuildDate>Thu, 23 Dec 2021 18:09:46 GMT</lastBuildDate><atom:link href="http://multix.jp/author/askn/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[WebBrowserコンポーネントいろいろ]]></title><description><![CDATA[<p>WebBrowserコンポーネントを .NET Framework であれこれ料理したときのいろいろ。</p>

<ul class="index"></ul>

<hr>

<h3 id="">レンダリングバージョンを変更する</h3>

<p>WebBrowserコンポーネント<sup id="fnref:1"><a href="http://multix.jp/webbrowser-component-etc/#fn:1" rel="footnote">1</a></sup>は、何もせずにそのまま使用すると必ず IE7相当のレンダリングエンジンと JavaScriptエンジンになってしまう。これは <code>&lt;meta http-equiv="x-ua-compatible" content="IE=Edge" /&gt;</code> などを食わせても変わらない。<sup id="fnref:2"><a href="http://multix.jp/webbrowser-component-etc/#fn:2" rel="footnote">2</a></sup> この問題を解決するには、レジストリを触るしか方法がない。</p>

<pre><code class="language-brush:vb ">Try  
    Dim RendererVersion As Integer = 11000
    Dim ExecName As String = Path.GetFileName(Environment.GetCommandLineArgs()(0))
    Dim Regkey As RegistryKey = _
        Registry.CurrentUser.CreateSubKey( _
            "SOFTWARE\Microsoft\Internet Explorer\</code></pre>]]></description><link>http://multix.jp/webbrowser-component-etc/</link><guid isPermaLink="false">54302723-107c-4a26-93fd-27768651728a</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[VB.NET]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 17 Apr 2017 09:22:12 GMT</pubDate><content:encoded><![CDATA[<p>WebBrowserコンポーネントを .NET Framework であれこれ料理したときのいろいろ。</p>

<ul class="index"></ul>

<hr>

<h3 id="">レンダリングバージョンを変更する</h3>

<p>WebBrowserコンポーネント<sup id="fnref:1"><a href="http://multix.jp/webbrowser-component-etc/#fn:1" rel="footnote">1</a></sup>は、何もせずにそのまま使用すると必ず IE7相当のレンダリングエンジンと JavaScriptエンジンになってしまう。これは <code>&lt;meta http-equiv="x-ua-compatible" content="IE=Edge" /&gt;</code> などを食わせても変わらない。<sup id="fnref:2"><a href="http://multix.jp/webbrowser-component-etc/#fn:2" rel="footnote">2</a></sup> この問題を解決するには、レジストリを触るしか方法がない。</p>

<pre><code class="language-brush:vb ">Try  
    Dim RendererVersion As Integer = 11000
    Dim ExecName As String = Path.GetFileName(Environment.GetCommandLineArgs()(0))
    Dim Regkey As RegistryKey = _
        Registry.CurrentUser.CreateSubKey( _
            "SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION")
    Regkey.SetValue(ExecName, RendererVersion)
    Regkey.Close()
Catch ex As Exception  
    Console.WriteLine("CreateSubKey: " &amp; ex.StackTrace)
End Try

WebBrowser1 = New WebBrowser()  
WebBrowser1.ObjectForScripting = Me  
</code></pre>

<p>レジストリには、キー＝実行ファイル(EXE)名、値＝IEバージョンの1000倍のDWORD値 を登録する。存在しない時の既定値は 7000 と見做される。またこのとき Visual Studio のデバッグRunでは .EXE名が <code>〜.vhost.exe</code> になるのを考慮する必要がある。結局のところ、プログラム実行毎に毎回自分自身の .EXE名を新規／上書き登録してしまうのが手堅い。またレジストリの登録タイミングは初めて <code>New WebBrowser()</code> する以前でなければならない。</p>

<hr>

<h3 id="vbnet">マウスイベントを VB.NET側で使いたい</h3>

<p>WebBrowserコンポーネント内で発生したイベントはほぼすべて JavaScript内で完結して外部（Windows.Forms）に伝播しない。必要なら JavaScriptから ObjectForScripting機能経由で Formクラスを呼んで RaiseEvent しろということになっている。だがそういう毎回使うような定形処理はサブクラス化してまとめといたほうが良いに決まっている。</p>

<pre><code class="language-brush:vb title: WebBrowserEx.vb">Imports System.Windows.Forms  
Public Class WebBrowserEx  
    Inherits WebBrowser

    Private WithEvents Body As HtmlElement

    Public Shadows Event MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs)
    Public Shadows Event MouseLeave(ByVal sender As Object, ByVal e As MouseEventArgs)

    Protected Overrides Sub OnDocumentCompleted(ByVal e As WebBrowserDocumentCompletedEventArgs)
        Me.Body = Me.Document.Body
        MyBase.OnDocumentCompleted(e)
    End Sub

    Private Sub Body_MouseDown(ByVal sender As Object, _
                               ByVal e As HtmlElementEventArgs) _
                           Handles Body.MouseDown
        RaiseEvent MouseDown(sender, New MouseEventArgs(e.MouseButtonsPressed, 1, _
                                                        e.ClientMousePosition.X, _
                                                        e.ClientMousePosition.Y, 0))
    End Sub

    Private Sub Body_MouseLeave(ByVal sender As Object, _
                                ByVal e As HtmlElementEventArgs) _
                            Handles Body.MouseLeave
        RaiseEvent MouseLeave(sender, New MouseEventArgs(MouseButtons.None, 0, _
                                                         e.ClientMousePosition.X, _
                                                         e.ClientMousePosition.Y, 0))
End Class

' WebBrowser1 = New WebBrowserEx()  
' WebBrowser1.Navigate("about:blank")  
</code></pre>

<p>ここでは一部しか書いてないが、Up/Enter/Move/Over も同様に実装<sup id="fnref:3"><a href="http://multix.jp/webbrowser-component-etc/#fn:3" rel="footnote">3</a></sup>できる。</p>

<p>ただこの実装においては、WebBrowserコンポーネント内で Refresh() する、あるいはF5(Ctrl+R)キーを押してのページリロードに対応できない。というのも .NET版の WebBrowserコンポーネントはリロード時に一切イベントを発火しないからだ。ページ内のJavaScriptでは通常通り onload等が発火するので ObjectForScriptingで補完する（Me.Document.Body を読み直す）必要がある。</p>

<hr>

<h3 id="jscripttypeinfonet">JScriptTypeInfo と .NET オブジェクトの相互変換</h3>

<p>ObjectForScriptingと InvokeScriptを使えば .NETと JavaScriptのユーザ関数を相互に呼び出し合うことが出来る。この時確実に引き渡せる引数は、数値・文字列・真偽値・null の4種類しかない。配列や連想配列（ハッシュ）はシリアライズする必要がある。だが実際には次のコードを試せば解るが、JavaScriptで生成された複雑なデータ構造は .NETのなかを素通りさせることができる。</p>

<pre><code class="language-brush:javascript title:JavaScript">var r = window.external.foo({a:[123, 456], h:{s:789}, s:"Test"});  
function bar (data) {  
    return {b:data.a[0] + data.h.s};
}
console.log(r.b); // 912  
</code></pre>

<pre><code class="language-brush:vb title:VB.NET">Public Function foo (ByVal d As Object) As Object  
    Console.log(d.s)    ' Test
    Console.log(d.a(1)) ' 456
    Console.log(d.h.s)  ' 789
    Return WebBrowser1.Document.InvokeScript("bar", d)
End Function  
</code></pre>

<p>WebBrowserコンポーネントにはこのJavaScriptネイティブのデータ構造＝JScriptTypeInfo型オブジェクトを解釈する機能（オブジェクト定義）がない。単純な文字列や数値は ToString()や ToInt32()メソッドで変換できるし、ハッシュの場合は Item() メソッドこそ使えるものの Count() も Containes() もないので「静的な構造体」以外は直接扱えないのだ。</p>

<pre><code class="language-brush:vb">Console.log(d.s)               ' sメンバーがあれば読み出せるがなければ例外発生  
Console.log(d.Item("s"))       ' これもメンバーがなければ例外発生  
Console.log(d.Conatines("s"))  ' これもメンバーがなければ例外発生  
</code></pre>

<p>.NETのほうでは3.5以降で JSON文字列をシリアライズ／デシリアライズ出来るため、引数も返値も JSON文字列化する取り決めにすれば良い話なのだが、ネイティブにデータ構造を受け渡しできたほうが（JavaScript側の視点では）利便性が良い。もっぱら融通が効かないのは静的型言語である .NETの側なので、こちらをもう少し頑張ってみよう。</p>

<p>取り敢えず、JScriptTypeInfoを読んで比較的扱いやすい Dictionary型にまるごと変換するなら次のようなコードが書ける。いったん IExpando型にキャストすればメンバーのプロパティリストを得られるので、CallByName()で実体を得ることができる。これを再帰的に適用すればいちおう全体にアクセスすることが出来る。ただし配列については手を抜いてハッシュ化してしまっているが。</p>

<pre><code class="language-brush:vb title:JScriptTypeInfo型を Dictionary型に変換する（暫定）">' Imports System.Reflection  
' Imports System.Runtime.InteropServices.Expando

Private Function JscInfoToDict(ByVal jscinfo As Object) As Dictionary(Of String, Object)  
    Dim dict = New Dictionary(Of String, Object)
    Dim keys() As String = DirectCast(jscinfo, IExpando).GetProperties( _
        BindingFlags.Default).Select(Function(p) p.Name).ToArray()
    For Each Key In keys
        Dim Var As Object = CallByName(jscinfo, Key, CallType.Get)
        ' Console.WriteLine("Key:" &amp; Key &amp; ", Var:" &amp; Var.ToString &amp; ", Type:" &amp; Var.GetType.ToString)
        If Var.GetType.ToString = "System.__ComObject" Then  ' これだけでは配列とハッシュの区別がつけられない
            dict.Add(Key, JscInfoToDict(Var))
        Else
            dict.Add(Key, Var)
        End If
    Next
    Return dict
End Function  
</code></pre>

<p>これとは逆に、Dictionary型を JScriptTypeInfo型に変換するのは、JScriptTypeInfo型の定義がないので不可能だ。ならば「JScriptTypeInfo型を知っている」言語に変換を委託してしまえば良い。そもそも相互変換をしたい場面において WebBrowserコンポーネントを使っていないということは滅多にないだろうから、InvokeScriptで JSONクラスメソッドを呼び出してしまえば良い。<sup id="fnref:4"><a href="http://multix.jp/webbrowser-component-etc/#fn:4" rel="footnote">4</a></sup></p>

<pre><code class="language-brush:vb title:相互変換">' Imports System.Web.Script.Serialization

' Friend WithEvents Converter As WebBrowser  
' Converter = New WebBrowser()  
' Converter.Navigate(New Uri("about:blank"))

Private Sub Converter_DocumentCompleted(ByVal sender As Object, _  
                                        ByVal e As EventArgs) _
                                    Handles Converter.DocumentCompleted
    sender.Document.InvokeScript("eval", New Object() {"function __j2s(j){return JSON.stringify(j)};"})
    sender.Document.InvokeScript("eval", New Object() {"function __s2j(j){return JSON.parse(j)};"})
End Sub

Public Function jobject_decoder(ByVal jscinfo As Object) As Dictionary(Of String, Object)  
    Dim jss As JavaScriptSerializer = New JavaScriptSerializer()
    Dim jstring As String = Converter.Document.InvokeScript("__j2s", New Object() {jscinfo})
    Return jss.Deserialize(Of Dictionary(Of String, Object))(If(jstring, "{}"))
End Function

Public Function jobject_encoder(ByVal dict As Dictionary(Of String, Object)) As Object  
    Dim jss As JavaScriptSerializer = New JavaScriptSerializer()
    Dim jstring As String = jss.Serialize(dict)
    Return Converter.Document.InvokeScript("__s2j", New Object() {jstring})
End Function  
</code></pre>

<p>.NET版の InvokeScript() は COM版のそれとは異なり、ルート要素のユーザ関数しか呼び出せず、クラスメソッドを直接叩くことが出来ない。本来なら <code>JSON.stringify</code> <code>JSON.parse</code> を直接使いたいが使えないので、DocumentCompleted を待ってから変換用ユーザ関数を eval で登録する方法を取った。なお JSONクラスは IE7ではそもそも存在しないので、前述した IEバージョン変更のレジストリ登録が事前に必要になる。<sup id="fnref:5"><a href="http://multix.jp/webbrowser-component-etc/#fn:5" rel="footnote">5</a></sup></p>

<hr>

<h3 id="cscriptexejson">余談：cscript.exeでの JSON変換</h3>

<p>Windows標準でかつ標準入出力が使える汎用インタプリタとして、cscript.exe は貴重な存在だ。うまく使えば grep/sed/awk あたりの代用にもなる。だが標準では JSON変換機能を持っていないなど基本的な言語仕様が少々古い。しかし COMオブジェクトとして HTMLファイルにアクセスすると、その中の最新 JavaScriptエンジンから欲しい機能をアドオン的に持ってくることが出来る。</p>

<pre><code class="language-brush:javascript title:jconvert.js">// Ex) cscript.exe //Nologo //U jconvert.js &lt;STDIN &gt;STDOUT
var htmlfile = WScript.CreateObject('htmlfile'), JSON;  
htmlfile.write('&lt;meta http-equiv="x-ua-compatible" content="IE=9" /&gt;');  
htmlfile.close(JSON = htmlfile.parentWindow.JSON);

while (!WScript.StdIn.AtEndOfStream) {  
    var json = JSON.parse(WScript.StdIn.ReadLine()),
    // user code
    WScript.StdOut.WriteLine(JSON.stringify(json));
}
</code></pre>

<p>このテクニックは色々と応用が効く。同じようにして（役に立つかどうかはともかく）jQueryをインポートすることが出来るし、include／require文代わりに使うことも出来る。</p>

<p>なお cscript.exe の標準入出力は、素で起動すると ANSI すなわち日本語では CP932 になる。このため JSON文字列中のマルチバイト文字はエスケープされていなければ正しく扱えない。そこで //U オプション付きで起動すると UTF-16LE でマルチバイト文字を扱えるようになるが、改行コードも2バイト文字（ワード幅）にしなければならない罠がある。更に余談となるが、cscript.exe のスクリプトコードは、ANSI(CP932)または UTF-16LE（BOM付き）で記述されていなければならない。一般的な UTF-8N では文字化けが発生する。<sup id="fnref:6"><a href="http://multix.jp/webbrowser-component-etc/#fn:6" rel="footnote">6</a></sup></p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>ここでのサンプルコードは VB.NET だが、C#NETでも JScript.NET でも基本は変わらない。.NETではどの言語でコードを記述しようと、結果的に生成されるのは同一の共通言語オブジェクトだ。なお .NET Frameworkのバージョンは 4.0以降を前提とする。いまさら Vista 以前を動作対象にすることもないし、Win7でも 4.0 Runtime を入れれば済むことだ。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:1" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:2"><p>x-ua-compatible が実装されたのはIE8以降なのだから当然なのだが。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:2" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:3"><p>UpはDownと、Enter/Move/OverはLeaveとおなじ MouseEventArgs(...) 宣言を書く。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:3" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:4"><p>ここで使っている Converterはコントロール表示には使わないので Windows.Forms.Controls に Add する必要はない。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:4" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:5"><p>IEバージョンを変えないならば、Navigate() で読み込むページのなかのほうに、JSON変換関数を JavaScriptで自作して記述することになる。もっともそこまでの手間を掛けるなら、.NET側で頑張るよりも JavaScriptの方で普通に JSON変換して受け渡したほうがコード記述量は増えるものの、単純だろう。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:5" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:6"><p>これに気付いておらず「wscript／cscript は日本語が扱えない」とする発言や書籍がままあるので注意を要する。Windows本来のネイティブ文字コードは結構な昔から「ANSIおよびUnicode(16)」と決まっている。「メモ帳」が UTF-8Nファイルをそのまま保存せず、おせっかいにもBOMを挿入したりする所以もまたこれである。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:6" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[ActivePerlのオフライン構築]]></title><description><![CDATA[<p>ActivePerl 本体は実行インストーラで何時でもインストールできるが、ppm や cpan で組み込む追加モジュールはオンライン構築が前提になっている。だが世間から断絶された秘匿ネットワークや、そもそもインターネットに接続するという概念のない PCやサーバに納品したり、機材リプレースで開発環境を再構築しなければならない場面では、USBメモリや DVD-Rからのオフラインインストールが出来なければ話にならない。そもそもバージョンが数世代古くなるとダウンロード元の ppmレポジトリが有料プラン専用に切り替えられて、いつもで気軽に再ダウンロードすることができなくなってしまう。それでは10年以上メンテ契約が続くような案件では、開発当時の状況保存は結構重要な問題になるので予防線を張っておくことは必要なのだ。</p>

<hr>

<h3 id="ppmx">.ppmxファイルの保存</h3>

<p>ppm install を実行すると、レポジトリから .ppmxファイルが tempディレクトリにダウンロードされてインストールされる。だがこのファイルはコマンド終了時に全削除されるため、普段目にすることがない。.ppmxファイル自体は ppm install の引数に渡すことでオフラインインストールできるため、消される前にファイルのコピーを別の場所に取っておけば、あとでおなじ環境を再構築することが容易くなる。</p>

<pre><code class="language-brush:perl gutter:true title:Client.patch">--- C:/Perl64/lib/ActivePerl/PPM/Client.pm      Tue Feb 16 06:02:11 2016
+++ D:/Perl64/lib/</code></pre>]]></description><link>http://multix.jp/activeperl-offline-installation/</link><guid isPermaLink="false">db8a724c-e2cd-49a8-9217-2a6cf62cc337</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[ActivePerl]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 10 Mar 2017 04:08:26 GMT</pubDate><content:encoded><![CDATA[<p>ActivePerl 本体は実行インストーラで何時でもインストールできるが、ppm や cpan で組み込む追加モジュールはオンライン構築が前提になっている。だが世間から断絶された秘匿ネットワークや、そもそもインターネットに接続するという概念のない PCやサーバに納品したり、機材リプレースで開発環境を再構築しなければならない場面では、USBメモリや DVD-Rからのオフラインインストールが出来なければ話にならない。そもそもバージョンが数世代古くなるとダウンロード元の ppmレポジトリが有料プラン専用に切り替えられて、いつもで気軽に再ダウンロードすることができなくなってしまう。それでは10年以上メンテ契約が続くような案件では、開発当時の状況保存は結構重要な問題になるので予防線を張っておくことは必要なのだ。</p>

<hr>

<h3 id="ppmx">.ppmxファイルの保存</h3>

<p>ppm install を実行すると、レポジトリから .ppmxファイルが tempディレクトリにダウンロードされてインストールされる。だがこのファイルはコマンド終了時に全削除されるため、普段目にすることがない。.ppmxファイル自体は ppm install の引数に渡すことでオフラインインストールできるため、消される前にファイルのコピーを別の場所に取っておけば、あとでおなじ環境を再構築することが容易くなる。</p>

<pre><code class="language-brush:perl gutter:true title:Client.patch">--- C:/Perl64/lib/ActivePerl/PPM/Client.pm      Tue Feb 16 06:02:11 2016
+++ D:/Perl64/lib/ActivePerl/PPM/Client.pm      Fri Mar 10 12:12:03 2017
@@ -1328,6 +1328,9 @@
                    if ($save_len != $len) {
                        die "Aborted download ($len bytes expected, got $save_len).\n";
                    }
+use File::Copy;
+my $File = File::Basename::basename($save, '.tgz');
+File::Copy::copy($save, $File.'.ppmx');
                }
                # XXX An MD5 checksum for the tarball would be a good thing
            }
</code></pre>

<p>5.18以降に通用するこのパッチを当てると、カレントディレクトリにダウンロードされた .ppmxファイルが残されるようになる。依存関係で複数のモジュールが取得された場合はその全てが残される。このコードを見れば解るが、 .ppmxファイルの実態は単なる .tgzアーカイブだ。わざわざ拡張子を代えているのは、そうしないと ppmコマンドがオフラインインストール用ファイル名として認識しないからである。</p>

<p>なお ppmコマンドは自前のデータベースとキャッシュでダウンロード履歴を管理しており、毎回必ずしもファイルダウンロードが発生するわけではない。情報は以下のフォルダに保存されているので、作業前にこれを空にしてやればよい。</p>

<pre><code>%LOCALAPPDATA%\ActiveState\ActivePerl
</code></pre>

<p>保存しておいた .ppmxファイルは、ppm install でインストールすることが出来るが、依存関係にあるサブモジュールを先にインストールしておかないと、オンラインで取りに行こうとしてしまう。そこで複数の .ppmxファイルから .ppdファイルを取り出し、そこに書かれた依存関係リストを読み出し、優先順位を付けて順次処理することになる。</p>

<pre><code class="language-brush:perl gutter:true title:LocalUpdate.pl">#!C:/Perl/bin/perl.exe
use utf8;  
use strict;  
use Archive::Tar;  
use File::Basename;  
use File::Find;  
use Cwd;

my $cwd = $ARGV[0] // getcwd;  
print "Search directory: ", $cwd, "\n";  
my(@ppmx_list, %ppmx_hash, %ppmx_path);  
find(sub {  
    if (m/\.ppmx$/io) {
        $ppmx_path{$_} = $File::Find::name;
        push @ppmx_list, $_;
    }
}, $cwd);
foreach my $ppmx (@ppmx_list) {  
    $ppmx_hash{$ppmx} = 9999;
}
foreach my $ppmx (@ppmx_list) {  
    my $tar = Archive::Tar-&gt;new();
    $tar-&gt;read($ppmx_path{$ppmx}, 1);
    foreach my $file ($tar-&gt;get_files()) {
        if ($file-&gt;{name} =~ /\.ppd$/io) {
            foreach my $line (split /\n/, $file-&gt;{data}) {
                if (my $require = ($line =~ m/REQUIRE NAME="([^\"]+)"/o)[0]) {
                    $require =~ s/::/-/go;
                    foreach my $hash (keys %ppmx_hash) {
                        $ppmx_hash{$hash}-- if 0 == index lc($hash), lc($require);
                    }
                }
            }
        }
    }
}
undef @ppmx_list;  
while (my($key, $var) = each %ppmx_hash) {  
    push @ppmx_list, sprintf "%04u %s", $var, $key;
}
foreach my $ppmx (sort {lc $a cmp lc $b} @ppmx_list) {  
    $ppmx =~ s/^\d+ //o;
    my $command = "ppm install ".$ppmx_path{$ppmx}." --nodeps";
    print $cwd, "&gt; ", $command, "\n";
    system $command and die "$!";
    print "\n";
}

1;  
__END__  
</code></pre>

<p>このコードは .ppmxファイルを含む親ディレクトリを引数に取る。引数は複数でもよく、省略されればカレントディレクトリを起点にする。-nオプションは ppmコマンドに --nodeps を付加する。-n オプションなしで起動すると依存解決に失敗した場合（依存する .ppmxが見つからない場合）レポジトリからのダウンロードが生じる。</p>

<hr>

<h3 id="ppmx">.ppmxファイルの作成</h3>

<p>.ppmxファイルの実態は .tgzで固められた .ppdファイルと blibディレクトリだ。cpanでソースからビルドしたものを .ppmxファイルにして保存しておくと、再ビルドする手間を減らせる。cpanの作業ワークは <code>C:\Perl64\cpan\build</code> または <code>C:\Perl\cpan\build</code> にある。</p>

<p>例えば既に cpan install に成功した <code>Foo::Bar</code> モジュールに対応する <code>Foo-Bar-0.1010-4UofsT</code> というディレクトリがあったなら、まずそこに cd して <code>dmake ppd</code> を実行<sup id="fnref:1"><a href="http://multix.jp/activeperl-offline-installation/#fn:1" rel="footnote">1</a></sup>する。すると <code>Foo-Bar.ppd</code> というファイルが作成されるだろう。</p>

<pre><code class="language-brush:bash">&gt; cd C:\Perl64\cpan\build\Foo-Bar-0.1010-4UofsT
&gt; dmake ppd
&gt; type Foo-Bar.ppd
&lt;SOFTPKG NAME="Foo-Bar" VERSION="0.1010"&gt;  
    &lt;ABSTRACT&gt;foobar foobar&lt;/ABSTRACT&gt;
    &lt;AUTHOR&gt;Charlie Root &amp;lt;foobar@example.com&amp;gt;&lt;/AUTHOR&gt;
    &lt;IMPLEMENTATION&gt;
        &lt;PERLCORE VERSION="5,006,0,0" /&gt;
        &lt;REQUIRE NAME="Carp::" /&gt;
        &lt;REQUIRE NAME="Cwd::" VERSION="3.16" /&gt;
        &lt;REQUIRE NAME="Exporter::" /&gt;
        &lt;REQUIRE NAME="strict::" /&gt;
        &lt;REQUIRE NAME="vars::" /&gt;
        &lt;ARCHITECTURE NAME="MSWin32-x64-multi-thread-5.24" /&gt;
        &lt;CODEBASE HREF="" /&gt;
    &lt;/IMPLEMENTATION&gt;
&lt;/SOFTPKG&gt;  
</code></pre>

<p>REQUIRE 行は依存をあらわす。ARCHITECTURE 行はインストール先のプラットフォームを限定するものだが、ピュアPerlで書かれた環境非依存モジュールの場合は <code>noarch</code> に書き換えることでインストール先を選ばないようにすることもできる。</p>

<pre><code class="language-brush:xml ">&lt;ARCHITECTURE NAME="noarch" /&gt;  
</code></pre>

<p>.ppmx(.tgz)ファイルを作るための tar+gzアーカイバは、C:\Perl64\bin に <code>ptar.bat</code>としてインストールされている。すでにパスは通っているので次のようにすれば .ppmxファイルができあがる。</p>

<pre><code class="language-brush:bash ">&gt; ptar -v -c -C -z -f foo-bar.ppmx blib foo-bar.ppd
</code></pre>

<p>この際注意が必要なのは、.ppmxと .ppdのファイル名は一致させておかなければならない点だ。.ppmxファイル名にバージョン番号を付加する一般的なルールに従う場合は、.ppdファイル名にもバージョン番号を付加しておく。もちろんそのバージョン番号は SOFTPKG行の VERSIONと一致しているべきだ。</p>

<pre><code class="language-brush:bash ">&gt; ren foo-bar.ppd foo-bar-0.1010.ppd
&gt; ptar -v -c -C -z -f foo-bar-0.1010.ppmx blib foo-bar-0.1010.ppd
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>dmake.exe は cpan初期化時に C:\Perl64\sit\bin にインストールされているはずだ。 <a href="http://multix.jp/activeperl-offline-installation/#fnref:1" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[SOAP::Transport::HTTP がおかしい]]></title><description><![CDATA[<p>Perl 5.20.3 + SOAP::Lite ver1.20 で XQuery 投げるとなぜか落ちる。調査すると STDIN に対して Bareward エラーが。それでこういう場当たり的なパッチが必要になった。おいおい？</p>

<pre><code class="language-brush:perl gutter:true">--- C:\Perl64\site\lib\SOAP\Transport\HTTP.pm    2016-12-17 10:45:40.000000000 +0900
+++ D:\Perl64\site\lib\SOAP\Transport\HTTP.pm    2017-01-23 12:42:33.000000000 +0900
@@ -586,6 +586,</code></pre>]]></description><link>http://multix.jp/soap-lite-failed/</link><guid isPermaLink="false">59c25a8e-8ac7-4a28-bce5-3b3bf7ed3827</guid><category><![CDATA[めもらんだむ]]></category><category><![CDATA[Web]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 24 Feb 2017 01:24:43 GMT</pubDate><content:encoded><![CDATA[<p>Perl 5.20.3 + SOAP::Lite ver1.20 で XQuery 投げるとなぜか落ちる。調査すると STDIN に対して Bareward エラーが。それでこういう場当たり的なパッチが必要になった。おいおい？</p>

<pre><code class="language-brush:perl gutter:true">--- C:\Perl64\site\lib\SOAP\Transport\HTTP.pm    2016-12-17 10:45:40.000000000 +0900
+++ D:\Perl64\site\lib\SOAP\Transport\HTTP.pm    2017-01-23 12:42:33.000000000 +0900
@@ -586,6 +586,7 @@
         #my $content = q{};
         if ( !$chunked ) {
             my $buffer;
+            no strict 'subs';
             binmode(STDIN);
             if ( defined $ENV{'MOD_PERL'} ) {
                 while ( read( STDIN, $buffer, $length ) ) {
</code></pre>

<p>直接原因はよくわからんがとにかくこうすれば正常動作はした。STDIN が <code>strict subs</code> に引っかかるって通常ではちょっと考えにくいところだが？</p>

<hr>]]></content:encoded></item><item><title><![CDATA[windowsで名前付きパイプを作る]]></title><description><![CDATA[<p>ActivePerl で Win32::Pipe が正常動作しなかった（AutoLoaderが実行時に異常終了する）ので Win32::API を直接叩いてみた。</p>

<hr>

<p>クライアント側は普通に <em>open/close</em> で読み書きできるので、サーバ側で namedpipe を作る部分を書いてみる。</p>

<pre><code class="language-brush:perl gutter:true title:win32pipeserver.pl">use strict;  
use warnings;  
use Win32::API;  
use Fcntl;  
use constant {  
    PIPE_ACCESS_INBOUND     =&gt; 0x00000001,  # O_RDONLY相当
    PIPE_ACCESS_OUTBOUND    =&gt; 0x00000002,  # O_WRONLY相当
    PIPE_ACCESS_DUPLEX      =&gt; 0x00000003,  # O_RDWR相当

    PIPE_</code></pre>]]></description><link>http://multix.jp/win32-create-namedpipe/</link><guid isPermaLink="false">50f30e5a-4293-4576-84e3-6753b5ef8ec4</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 20 Feb 2017 09:33:48 GMT</pubDate><content:encoded><![CDATA[<p>ActivePerl で Win32::Pipe が正常動作しなかった（AutoLoaderが実行時に異常終了する）ので Win32::API を直接叩いてみた。</p>

<hr>

<p>クライアント側は普通に <em>open/close</em> で読み書きできるので、サーバ側で namedpipe を作る部分を書いてみる。</p>

<pre><code class="language-brush:perl gutter:true title:win32pipeserver.pl">use strict;  
use warnings;  
use Win32::API;  
use Fcntl;  
use constant {  
    PIPE_ACCESS_INBOUND     =&gt; 0x00000001,  # O_RDONLY相当
    PIPE_ACCESS_OUTBOUND    =&gt; 0x00000002,  # O_WRONLY相当
    PIPE_ACCESS_DUPLEX      =&gt; 0x00000003,  # O_RDWR相当

    PIPE_WAIT               =&gt; 0x00000000,
    FILE_FLAG_OVERLAPPED    =&gt; 0x40000000,
};

*STDOUT-&gt;autoflush;

my $PIPE_NAME = "\\\\.\\pipe\\pipesv";

my $CreateNamedPipe = Win32::API-&gt;new( "kernel32", "CreateNamedPipe", "PNNNNNNP", "N" ) or die;  
my $PIPE = $CreateNamedPipe-&gt;Call(  
    $PIPE_NAME,                #  LPCTSTR lpName,                             // パイプ名
    PIPE_ACCESS_DUPLEX,     #  DWORD dwOpenMode,                           // パイプを開くモード
    PIPE_WAIT,              #  DWORD dwPipeMode,                           // パイプ固有のモード
    255,                    #  DWORD nMaxInstances,                        // インスタンスの最大数
    0,                      #  DWORD nOutBufferSize,                       // 出力バッファのサイズ
    0,                      #  DWORD nInBufferSize,                        // 入力バッファのサイズ
    10_000,                 #  DWORD nDefaultTimeOut,                      // タイムアウト(msec)の間隔
    0,                      #  LPSECURITY_ATTRIBUTES lpSecurityAttributes  // セキュリティ記述子
);
my $CloseHandle = Win32::API-&gt;new( "kernel32", "CloseHandle", "N", "N" ) or die;

my $ReadFile = Win32::API-&gt;new( "kernel32", "ReadFile", "NPNPP", "N" ) or die;  
sub ReadFile {  
    my $hFile = shift;
    my $lpBuffer = "\0" x 512;
    my $nNumberOfBytesToRead = 512;
    my $lpNumberOfBytesRead = "\0" x 8;
    my $result = $ReadFile-&gt;Call(
        $hFile,
        $lpBuffer,
        $nNumberOfBytesToRead,
        $lpNumberOfBytesRead,
        0);
    return undef unless $result;
    my $length = unpack "V", $lpNumberOfBytesRead;
    return undef unless $length;
    return substr $lpBuffer, 0, $length;
}

my $WriteFile = Win32::API-&gt;new( "kernel32", "WriteFile", "NPNPP", "N" ) or die;  
sub WriteFile {  
    my $hFile = shift;
    my $lpBuffer = shift;
    while (my $nNumberOfBytesToWrite = defined $lpBuffer &amp;&amp; length $lpBuffer) {
        my $lpNumberOfBytesWritten = "\0" x 8;
        my $result = $WriteFile-&gt;Call(
            $hFile,
            $lpBuffer,
            $nNumberOfBytesToWrite,
            $lpNumberOfBytesWritten,
            0);
        return undef unless $result;
        my $length = unpack "V", $lpNumberOfBytesWritten;
        $lpBuffer = substr $lpBuffer, $length;
    }
    return undef;
}

my $ConnectNamedPipe = Win32::API-&gt;new( "kernel32", "ConnectNamedPipe", "NP", "N" ) or die;  
my $DisconnectNamedPipe = Win32::API-&gt;new( "kernel32", "DisconnectNamedPipe", "N", "N" ) or die;

while ($ConnectNamedPipe-&gt;Call($PIPE, 0)) {  
    WriteFile($PIPE, "Welcome\n");
    print ReadFile($PIPE);         # client from "Hello"
    $DisconnectNamedPipe-&gt;Call($PIPE);
    #last;      # no loop
}
$CloseHandle-&gt;Call($PIPE);

1;  
__END__  
</code></pre>

<p>本来はセキュリティ記述子を指定すべきだが、本件は動作サンプルなので省略<sup id="fnref:1"><a href="http://multix.jp/win32-create-namedpipe/#fn:1" rel="footnote">1</a></sup>する。バッファサイズについてはダミーなので 0 と書いて良い。
こうして出来た PIPEハンドル（単なるint値）は、PerlIOの感知するところではないため<sup id="fnref:2"><a href="http://multix.jp/win32-create-namedpipe/#fn:2" rel="footnote">2</a></sup> 通常の sysread/syswrite では扱えない。従って CloseHandle/WriteFile/ReadFile についても Win32API で実装する。</p>

<p><code>ConnectNamedPipe()</code> はソケット通信の accept() に相当する関数で、クライアント側が同名パイプを開くまでI/Oブロックする。<code>DisconnectNamedPipe()</code>はこのセッションを閉じてクライアントを切断する。</p>

<p>いっぽう、クライアント側は普通のファイルアクセスとなんら変わるところはない。</p>

<pre><code class="language-brush:perl gutter:true title:win32pipeclient.pl">use strict;  
use warnings;

*STDOUT-&gt;autoflush;

my $PIPE_NAME = "\\\\.\\pipe\\pipesv";

open my $FH, "+&lt;", $PIPE_NAME or die "$!";  
$FH-&gt;binmode(":raw");
print scalar &lt;$FH&gt;;                # server from "Welcome"  
$FH-&gt;write("hello\n");
$FH-&gt;close;

1;  
__END__  
</code></pre>

<p>マルチクライアントサーバを実装する場合、基本的には ConnectNamedPipe のあとで CreateThread すればよいが、CreateNamedPipe に FILE_FLAG_OVERLAPPED の指定と、ConnectNamedPipe に lpOverlapped の指定が必要になる。<sup id="fnref:3"><a href="http://multix.jp/win32-create-namedpipe/#fn:3" rel="footnote">3</a></sup></p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>従って誰でもどこからでも無制限に読み書きできてしまう。その場合何が起こり得るかについては<a href="http://eternalwindows.jp/windevelop/service/service06.html">こちら</a>が詳しい。 <a href="http://multix.jp/win32-create-namedpipe/#fnref:1" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:2"><p>open($DUP, "&lt;&amp;=", $fd) といった構文では認識できない。 <a href="http://multix.jp/win32-create-namedpipe/#fnref:2" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:3"><p><a href="https://msdn.microsoft.com/ja-jp/library/cc429611.aspx">https://msdn.microsoft.com/ja-jp/library/cc429611.aspx</a> <a href="http://multix.jp/win32-create-namedpipe/#fnref:3" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[textareaでTAB入力]]></title><description><![CDATA[<p><code>&lt;textarea/&gt;</code> のフォーカス内でTABキーを押すと普通は次のform部品へフォーカスが移ってしまうが、これをTAB入力に変換する。フォーカス動作はキャンセルしなければならないので keydown イベントを使う。</p>

<hr>

<pre><code class="language-brush:javascript gutter:true">// textareaでのTABキー入力
$('textarea').on('keydown', function(e){
    if (e.keyCode === 9) {
        e.preventDefault();
        var elem = e.target;
        var val = elem.value;
        var pos = elem.selectionStart;
        elem.value = val.substr(0, pos) + '\t' + val.substr(pos, val.length);
        elem.setSelectionRange(pos + 1, pos</code></pre>]]></description><link>http://multix.jp/tab-input-textarea/</link><guid isPermaLink="false">f0beb3a4-5add-4f20-85b1-c2f21b70f871</guid><category><![CDATA[めもらんだむ]]></category><category><![CDATA[Web]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 20 Feb 2017 09:31:33 GMT</pubDate><content:encoded><![CDATA[<p><code>&lt;textarea/&gt;</code> のフォーカス内でTABキーを押すと普通は次のform部品へフォーカスが移ってしまうが、これをTAB入力に変換する。フォーカス動作はキャンセルしなければならないので keydown イベントを使う。</p>

<hr>

<pre><code class="language-brush:javascript gutter:true">// textareaでのTABキー入力
$('textarea').on('keydown', function(e){
    if (e.keyCode === 9) {
        e.preventDefault();
        var elem = e.target;
        var val = elem.value;
        var pos = elem.selectionStart;
        elem.value = val.substr(0, pos) + '\t' + val.substr(pos, val.length);
        elem.setSelectionRange(pos + 1, pos + 1);
    }
});
</code></pre>

<ul>
<li><code>e.keyCode === 9</code> <br>
TABキー押し下げを検知する。</li>
<li><code>e.preventDefault()</code> <br>
ブラウザのデフォルト動作を停止する。つまりフォーカス移動をキャンセルする。</li>
<li><code>e.target</code> <br>
イベント対象のDOMエレメントを取得する。すなわち <code>&lt;textarea/&gt;</code> 本体。jQueryの <code>$(this)[0]</code> におなじ。</li>
<li><code>elem.value</code> <br>
入力されている <code>&lt;textarea/&gt;</code> の内容。</li>
<li><code>elem.selectionStart</code> <br>
キャレットカーソルの位置。次の行でこの位置に<code>\t</code>を挿入して書き戻している。</li>
<li><code>elem.setSelectionRange(Start, End, [Direction])</code> <br>
キャレットを<code>\t</code>の後ろへ進める。選択範囲は無いので <em>Start</em> と <em>End</em> にはおなじ値を指定する。</li>
</ul>

<p>なお <em>selectionStart/selectionEnd</em> プロパティは直接書き換えることもできる。またキャレット選択範囲の後方は <em>elem.selectionEnd</em> で得られる。なので選択範囲があるなら丸ごとインデントするといったコードも書けなくはない。選択方向を指示する Direction はプラットフォームによって挙動が若干異なるので<a href="http://www.html5.jp/tag/elements/forms-textFieldSelection.html">こちら</a><sup id="fnref:1"><a href="http://multix.jp/tab-input-textarea/#fn:1" rel="footnote">1</a></sup>も参照のこと。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p><a href="http://www.html5.jp/tag/elements/forms-textFieldSelection.html">http://www.html5.jp/tag/elements/forms-textFieldSelection.html</a> <a href="http://multix.jp/tab-input-textarea/#fnref:1" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[IIS用FastCGI簡易ラッパー]]></title><description><![CDATA[<p><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/">前項</a>でIIS用FastCGIについて述べた件の続き。</p>

<hr>

<p>モジュールマップ登録では個々の.fcgiファイルについて1エントリが必要とされるため、拡張子.fcgi全体をひとつのモジュールマップで済ませるための簡単なラッパーを書いてみた。ものすごく単純だが用法を誤らなければ相応に使える。</p>

<pre><code class="language-brush:perl gutter:true title:fcgi.pl">use strict;  
use warnings;  
use FCGI;  
use File::Spec;  
open STDERR, "&gt;&amp;", STDOUT;  
my $request = FCGI::Request or die;  
while ( $request-&gt;Accept &gt;= 0 ) {  
    my $stderr = '';
    eval {
        local $SIG{__WARN__} = sub { $stderr .= shift };
        local $SIG{__DIE__} = sub { die($stderr = shift)</code></pre>]]></description><link>http://multix.jp/iis-fastcgi-easy-wrapper/</link><guid isPermaLink="false">d447898a-80b8-401a-aab0-e76227fcb7f8</guid><category><![CDATA[Windows]]></category><category><![CDATA[Web]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 20 Feb 2017 09:25:55 GMT</pubDate><content:encoded><![CDATA[<p><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/">前項</a>でIIS用FastCGIについて述べた件の続き。</p>

<hr>

<p>モジュールマップ登録では個々の.fcgiファイルについて1エントリが必要とされるため、拡張子.fcgi全体をひとつのモジュールマップで済ませるための簡単なラッパーを書いてみた。ものすごく単純だが用法を誤らなければ相応に使える。</p>

<pre><code class="language-brush:perl gutter:true title:fcgi.pl">use strict;  
use warnings;  
use FCGI;  
use File::Spec;  
open STDERR, "&gt;&amp;", STDOUT;  
my $request = FCGI::Request or die;  
while ( $request-&gt;Accept &gt;= 0 ) {  
    my $stderr = '';
    eval {
        local $SIG{__WARN__} = sub { $stderr .= shift };
        local $SIG{__DIE__} = sub { die($stderr = shift) };
        my $file = File::Spec-&gt;catfile(
            $ENV{DOCUMENT_ROOT} // 'Undefined',
            $ENV{SCRIPT_NAME} // 'index.fcgi'
        );
        return print "Status: 404\n\n" unless -f $file;
        do $file;
    };
    $stderr = "$@" if $@;
    if ($stderr) {
        print "Status: 403\nContent-Type: text/plain\n\n", $stderr;
    }
}
1;  
__END__  
</code></pre>

<p>このラッパーを要求パス *.fcgi に対して適用する<sup id="fnref:1"><a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fn:1" rel="footnote">1</a></sup>と、ドキュメントルート以下の FastCGIファイルについて <strong>半常駐化</strong>状態で実行されるようになる。PATH_INFO は期待どうり動作するし、 <em>既定のドキュメント</em> に index.fcgi を追加した場合にも対応する。ただしあくまでも簡易的に、だ。</p>

<p>このラッパーがなぜ半常駐化なのかというと、常駐対象になるのが、特殊変数および <code>our</code> 宣言<sup id="fnref:2"><a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fn:2" rel="footnote">2</a></sup>されたパッケージ変数と <code>use</code> または <code>require</code> で読み込まれたモジュールに限定されているからだ。.fcgi ファイル自体は <code>do $file</code> の形式で読込実行されているが、これはこの命令が実行される度にファイルアクセスが発生し、コードが再コンパイル<sup id="fnref:3"><a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fn:3" rel="footnote">3</a></sup>されることによる。このとき <code>do $file</code> は <code>package main</code> 名前空間内で実行されているため、複数の .fcgi で同名のグローバル変数や関数宣言が共有されることになる。</p>

<p>.fcgi 自体は毎回再コンパイルされるが、その中で使われる use モジュールについてはコンパイル済コードが再利用されるため、その使用比率が大きいコードほどメリットがある。一方で名前空間のファイル別隔離処理を端折っているため同名関数は後続のコード実行で上書きされてしまうため注意を要する。このあたりが簡易版と称する所以だ。</p>

<p>エラー処理について言えば、FastCGI 起動前に STDERR を STDOUT にリダイレクトすることで、<code>do $file</code> 実行時の構文エラーが極力 IIS Worker Process へ渡らないように細工している。<code>$SIG{}</code> の定義も同様に STDERR を漏らさないためだ。これを怠ると IIS が <em>503 Service Unavailable</em> を返すようになり、かつ、サーバ自体を再起動しなければ回復できない<sup id="fnref:4"><a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fn:4" rel="footnote">4</a></sup>ことがままある。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>このラッパーそのものはドキュメントルートの外に配置する。また拡張子も.fcgi以外（.pl）とするべきだ。またこのラッパー自体は完全常駐状態となるため、リロードするためには IIS Worker Process を終了させなければならない。 <a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fnref:1" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:2"><p>our $count //= 0; などと書けば、未定義状態の初回ロード時だけ初期化される。レキシカル変数については eval $EXPR と違って異なるスコープとなる。よって my 宣言は重複しても影響がない。 <a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fnref:2" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:3"><p>require は再ロードも再コンパイルもしないが、再実行もしない。再コンパイルせずに再実行だけ行う関数はない。 <a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fnref:3" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:4"><p>IISサービス（W3SVC）を再起動するだけでは回復しない。$stderr の出力時に <em>Status: 500</em> としていないのも同じ理由による。これだけ念を入れても不用意な実行時エラー（よくあるのは再入不能モジュールの実行時エラー。例えば binmode Encoding関係や Win32関係など）でサービス停止に追い込まれる当たり、IIS の FastCGI 環境は結構繊細なので、エラーに無頓着なタイプの開発者には全く向いていない。 <a href="http://multix.jp/iis-fastcgi-easy-wrapper/#fnref:4" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[IIS8+FastCGI+ActivePerl+Mojolicious::Lite]]></title><description><![CDATA[<p>最近のWindows IISでMojolicious::Liteを動かそうとすると、普通はCGIモードしか使えなくて全く実用的なパフォーマンスが確保できない。これは双方が互いを全くサポートしていないが故だが、これはそれをどうにかしてしまおうというメモ。</p>

<ul class="index"></ul>

<h3 id="iiscgi">IISのCGIセットアップ</h3>

<p>まず普通にIISでPerl CGIを動かすための手順。一応 Windows 10/Windows Server 2012 と ActivePerl 5.20(64bit) を前提にしているが、他の組み合わせでも大差はない。</p>

<p><strong>コントロールパネル＞プログラム＞プログラムと機能＞Windowsの機能の有効化または無効化</strong>と辿ってダイアログを開き、<strong>インターネットインフォメーションサービス</strong>にチェックを付ける。更にそのサブメニューを開いて<strong>World Wide Webサービス＞アプリケーション開発機能>CGI</strong>にチェックを付ける。最低限これだけを有効化すれば要は足りるからOKを押して変更を確定する。初回導入時についてはサーバ再起動は必要ない。<sup id="fnref:1"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:1" rel="footnote">1</a></sup></p>

<p><img src="http://multix.jp/content/images/2015/12/2015-12-27-10-48-22.png" alt=""></p>

<p>ActivePerl(64bit)が既にインストールされており、パスが通っているならコマンドプロンプトを管理者権限で開いて以下のコマンドをタイプする。</p>

<pre><code class="language-brush:plain">ap-iis-config add all --site 1 --cgi  
</code></pre>

<p>するとIISマネージャーの<strong></strong></p>]]></description><link>http://multix.jp/iis8-fastcgi-mojolicious-lite/</link><guid isPermaLink="false">51d12cb6-9839-4b14-b74d-53400cfcb7e2</guid><category><![CDATA[Windows]]></category><category><![CDATA[Web]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Sun, 27 Dec 2015 04:33:31 GMT</pubDate><content:encoded><![CDATA[<p>最近のWindows IISでMojolicious::Liteを動かそうとすると、普通はCGIモードしか使えなくて全く実用的なパフォーマンスが確保できない。これは双方が互いを全くサポートしていないが故だが、これはそれをどうにかしてしまおうというメモ。</p>

<ul class="index"></ul>

<h3 id="iiscgi">IISのCGIセットアップ</h3>

<p>まず普通にIISでPerl CGIを動かすための手順。一応 Windows 10/Windows Server 2012 と ActivePerl 5.20(64bit) を前提にしているが、他の組み合わせでも大差はない。</p>

<p><strong>コントロールパネル＞プログラム＞プログラムと機能＞Windowsの機能の有効化または無効化</strong>と辿ってダイアログを開き、<strong>インターネットインフォメーションサービス</strong>にチェックを付ける。更にそのサブメニューを開いて<strong>World Wide Webサービス＞アプリケーション開発機能>CGI</strong>にチェックを付ける。最低限これだけを有効化すれば要は足りるからOKを押して変更を確定する。初回導入時についてはサーバ再起動は必要ない。<sup id="fnref:1"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:1" rel="footnote">1</a></sup></p>

<p><img src="http://multix.jp/content/images/2015/12/2015-12-27-10-48-22.png" alt=""></p>

<p>ActivePerl(64bit)が既にインストールされており、パスが通っているならコマンドプロンプトを管理者権限で開いて以下のコマンドをタイプする。</p>

<pre><code class="language-brush:plain">ap-iis-config add all --site 1 --cgi  
</code></pre>

<p>するとIISマネージャーの<strong>サイト＞Default Web Site＞機能ビュー＞ハンドラーマッピング</strong>に以下のスクリプトマップが登録されているはずだ。</p>

<p><img src="http://multix.jp/content/images/2015/12/2015-12-27-12-17-27.png" alt=""></p>

<p>復数のサイトが作られている場合、個別にこのスクリプトマップが必要になる。前述の<mark>--site 1</mark>の数字がサイトの登録番号を示しているので、これを変更しながら必要なだけ繰り返す。あるいはこのダイアログ画像を参考に手動でスクリプトマップを追加する。</p>

<p>なお32bit版のActivePerlでは、ISAPIを使用することもできる。Windows機能ダイアログで<strong>ISAPIフィルター</strong>と<strong>ISAPI拡張</strong>にもチェックをつけていれば、以下のコマンドでISAPIのハンドラーが作成できる。<sup id="fnref:2"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:2" rel="footnote">2</a></sup></p>

<pre><code class="language-brush:plain">ap-iis-config add all --site 1 --cgi --isapi  
</code></pre>

<p>以上で <em>.pl ファイルはCGIとして認識されるようになる。</em>.cgi ファイルを扱いたい場合は手動でスクリプトハンドラーを追加しよう。<sup id="fnref:3"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:3" rel="footnote">3</a></sup></p>

<h4 id="fastcgi">FastCGIを有効化する</h4>

<p>FastCGIも同様にハンドラーマッピングを追加することで有効化できるが、ひとつのスクリプトに対してひとつのモジュールマップエントリが必要になる。例えば以下のスクリプトを登録してみよう。<sup id="fnref:4"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:4" rel="footnote">4</a></sup></p>

<pre><code class="language-brush:perl gutter:true title:count.fcgi">use strict;  
use warnings;  
use FCGI;

my $count = 0;  
my $request = FCGI::Request();  
while ( $request-&gt;Accept &gt;= 0 ) {  
    print "Content-type: text/html\r\n\r\n", ++$count;
}
</code></pre>

<p>このファイルは通常のURIで辿れる場所に存在しなければならない。つまり<em>C:¥inetpub¥wwwroot</em>以下以外にあるのならば、仮想ディレクトリパスが通っている必要がある。そうした上で次のようにモジュールマップを登録する。</p>

<p><img src="http://multix.jp/content/images/2015/12/2015-12-27-12-44-28.png" alt=""></p>

<p>要求パス欄は実ファイル名と一致<sup id="fnref:5"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:5" rel="footnote">5</a></sup>させなければならない。モジュール欄はドロップダウンからFastCgiModuleを選択する。名前欄は同一サイト内で重複しなければ自由だ。そして実行可能ファイル欄に<strong>Perlの実行フルパス名</strong>と<strong>スクリプトのフルパス名"</strong>を半角パイプ記号で続けて記入する。なぜパイプで繋げるのかは解らないがこのように書かなければならない。これで<em>OK</em>を押すとハンドラを登録してよいか警告ダイアログが出るので<em>はい</em>で応答する。</p>

<p>正しくFastCGIハンドラが登録されていれば、このスクリプトをブラウザで開けば（例えば localhost/test/count.fcgi）カウンタが表示されるだろう。Ctrl+RやF5でリロードすればカウントアップが進むはずだ。</p>

<p>なおスクリプトを書き換えたあとは、実行中のFastCGIハンドラを再起動しなければならない。手っ取り早いのはタスクマネージャのプロセス一覧から、<em>IIS Worker Process</em> を探して <em>タスクの終了</em> をさせてしまう方法だ。こうすればすぐに再度1からカウントをやり直す。</p>

<h4 id="mojoliciouslitefastcgi">Mojolicious::LiteアプリをFastCGIで実行する</h4>

<p>Mojoliciousは、その開発当初はともかく現在はIISをサポートしていない。だが前述のもっとも単純な FastCGIスクリプトを見ればわかるように、Requestループの中で普通にCGIスクリプトを利用すれば、そのままFastCGIプロセス <em>(IIS Worker Process)</em> として永続化される。かといってループ内で Mojolicious を呼び出してもそのままでは正常に動作しない。app->start が IIS/CGIであるかどうかを正しく認識できないためだ。そこで次のようなコードにする。<sup id="fnref:6"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:6" rel="footnote">6</a></sup></p>

<pre><code class="language-brush:perl gutter:true title:apps.fcgi">use strict;  
use warnings;

use Mojolicious::Lite;

plugin 'basic_auth';

my $count = 0;

get '/' =&gt; sub { $_[0]-&gt;render(text =&gt; 'Non restricted area' . ++$count); };

under sub {  
    my ($c) = shift;
    return $c-&gt;basic_auth(
        realm =&gt; sub {
            if ( @_ &amp;&amp; ( "@_" eq 'foo bar' ) ) {
                $c-&gt;req-&gt;env-&gt;{REMOTE_USER} = $_[0];
                return 1;
            }
        }
    );
};

get '/rest' =&gt; sub {  
    $_[0]-&gt;render(text =&gt; 'Hello ' . $_[0]-&gt;req-&gt;env-&gt;{REMOTE_USER} . ++$count . join(",", @ARGV));
};

if ($ENV{APP_POOL_CONFIG}) {  
    use FCGI;
    my $request = FCGI::Request();
    while ( $request-&gt;Accept &gt;= 0 ) {
        app-&gt;start("cgi");
    }
}
else {  
    app-&gt;start();
}
</code></pre>

<p>これはIIS固有の環境変数が存在するならば、FCGI::Requestループ実行に切り替えるようにしている。そうでなければ、つまりコマンドプロンプトで実行すれば、従来通りの挙動となる。こうしておいたうえで、FastCGIハンドラには次のように指定する。</p>

<pre><code class="language-brush:plain">C:¥Perl64¥bin¥perl.exe|C:¥test¥apps.fcgi cgi -m production  
</code></pre>

<p>ここでCGI形式で実行することを強要し、かつproductionモードを強制する。このモード指定がないと developmentモードで実行されることになり、標準出力に非HTTPプロトコルな出力が混じってしまうので、結果IISはInternal Server Errorを返すことになってしまう。</p>

<p>apps.fcgiのある場所に logディレクトリを掘っておくとそこにログが吐き出されるので、モード指定がなくても一見問題が解決できるように見える。だが実運用段階ではこのログを定期的に削除する必要に迫られるので、ここではその方法は取らない。ログファイルは IIS Worker Process が握っているので、このプロセスを終了させないとログファイルを削除することができないのだ。他には IISの環境変数に MOJO_MODE を追加して回避する手段があるが、今回は設定が一箇所ですむ手法を取り上げた。<sup id="fnref:7"><a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fn:7" rel="footnote">7</a></sup></p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>機能を無効化した際はアンインストールのために再起動が必要になる。 <a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fnref:1" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:2"><p>64bit版ではISAPIはサポートされなくなった。もっともこれを使う機会じたいが滅多にないので、困る／困ったという話もおよそ聞いたことがない。 <a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fnref:2" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:3"><p>Strawberry Perlや Cygin Perl、その他CGIで使えるものはなんであれこのように手動で登録する。 <a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fnref:3" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:4"><p>mod_rewrite的な仮想パス機能ではないので、/などを指定することはできない。 <a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fnref:4" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:5"><p>FCGIモジュールは標準インストールされていないので事前に <em>ppm install FCGI</em> を実行しておく必要がある。 <a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fnref:5" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:6"><p>このサンプルはBASIC認証プラグインのテストも兼ねている。このプラグインは <em>ppm install Mojolicious-Plugin-BasicAuth</em> でインストールできる。ブラウザで apps.fcgi/rest を開くと認証ダイアログが出るのでユーザ名:foo パスワード:bar を入力してみて欲しい。 <a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fnref:6" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:7"><p>環境変数切り替えでは一見、復数のモードを切り替えて使い分けられるように見える。だがUnix環境と違ってIISマネージャをいちいち立ち上げないとその操作すら覚束ないのでメリットがない。結局必要なだけ個別のモジュールハンドラを登録してURIで使い分けたほうが簡単だったりする。 <a href="http://multix.jp/iis8-fastcgi-mojolicious-lite/#fnref:7" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[Allied Telesis CentreCOM の小技]]></title><description><![CDATA[<p>最近のアラテレは SDNへの注力もあって ATxシリーズへの世代移行が加速しており、昔ながらの CentreCOMシリーズは青息吐息な感があるけれども、かえってかつての高級機が手頃な値段でオークション等に出回るようになった。ここでは CentreCOM系では最後にして現役の AR5xxルータおよびそれと同族の 8xxxスイッチ関係の小技をいくつか取り上げる。<sup id="fnref:1"><a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fn:1" rel="footnote">1</a></sup></p>

<ul class="index"></ul>  

<hr>

<h4 id="">コンソールへのアラートを黙らせる</h4>

<p>AR系や87xx系でファイアウォールを有効にすると、デフォルトではルールにないパケットを受信する度にマネージャ権限以上でログインしているコンソール（async/telnet/ssh）にアラート通知が流れてしまう。</p>

<pre><code class="language-brush:plain">XXX.XXX.XXX.XXX attempting to use non-policy interface

Denial of service attack from XXX.XXX.XXX.XXX is finished  
</code></pre>

<p>設定作業中にこれらが次々に流れてゆくと作業の邪魔も甚だしい（電源を切りたくなる）ので、次のコマンドでコンソールへのアラートは止めてしまおう。</p>

<pre><code class="language-brush:plain">disable firewall notify=manager  
</code></pre>

<p>UnTagポートにTagパケットが入ってきた時も <code>attempting to use</code></p>]]></description><link>http://multix.jp/allied-telesis-centrecom-memorandom/</link><guid isPermaLink="false">ca593ede-78b8-4ffc-9364-73cbc0bf2005</guid><category><![CDATA[めもらんだむ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 27 Jul 2015 08:39:00 GMT</pubDate><content:encoded><![CDATA[<p>最近のアラテレは SDNへの注力もあって ATxシリーズへの世代移行が加速しており、昔ながらの CentreCOMシリーズは青息吐息な感があるけれども、かえってかつての高級機が手頃な値段でオークション等に出回るようになった。ここでは CentreCOM系では最後にして現役の AR5xxルータおよびそれと同族の 8xxxスイッチ関係の小技をいくつか取り上げる。<sup id="fnref:1"><a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fn:1" rel="footnote">1</a></sup></p>

<ul class="index"></ul>  

<hr>

<h4 id="">コンソールへのアラートを黙らせる</h4>

<p>AR系や87xx系でファイアウォールを有効にすると、デフォルトではルールにないパケットを受信する度にマネージャ権限以上でログインしているコンソール（async/telnet/ssh）にアラート通知が流れてしまう。</p>

<pre><code class="language-brush:plain">XXX.XXX.XXX.XXX attempting to use non-policy interface

Denial of service attack from XXX.XXX.XXX.XXX is finished  
</code></pre>

<p>設定作業中にこれらが次々に流れてゆくと作業の邪魔も甚だしい（電源を切りたくなる）ので、次のコマンドでコンソールへのアラートは止めてしまおう。</p>

<pre><code class="language-brush:plain">disable firewall notify=manager  
</code></pre>

<p>UnTagポートにTagパケットが入ってきた時も <code>attempting to use non-policy</code>と怒られるので、VLAN変更をしている最中にも、これをしておくと結構助かる。</p>

<p>なお<code>notify=all</code>にするとすべてのアラートが止まるが、ログにも残らなくなる。ファイアウォール設定が充分固まって逆に <code>FIRE ATACK</code> 通知で <code>sh log</code> が埋まるようになったら、全部止めたほうがむしろ都合が良いだろう。</p>

<hr>

<h3 id="">時系列逆順ログ</h3>

<p>コマンドラインヘルプ（?キーのあれ）では表示されないので気づきにくいが <code>show log reverse (sh log rev)</code> でログを時系列の逆順に見ることが出来る。最新のログだけ確認して Quitで抜けるのに便利だ。</p>

<pre><code class="language-brush:plain">&gt; sh log rev

Date/Time   S Mod  Type  SType Message  
-------------------------------------------------------------------------------
27 17:58:21 3 IPG  IPFIL DUMP  Received invalid DA 8.240.213.66&gt;22.103.89.96  
                               Prot=17 Int=ppp5
27 17:57:57 3 IPG  IPFIL FRAG  DirBcast Fail 6.102.80.241&gt;22.103.89.103  
                               Prot=6 Int=vlan12
27 17:57:43 3 IPG  IPFIL FRAG  DirBcast Fail 19.6.240.45&gt;22.103.89.103 Prot=6  
                               Int=vlan12
</code></pre>

<p>もちろん moduleと組み合わせて特定カテゴリのログだけ表示するときにも使える。</p>

<pre><code class="language-brush:plain">&gt; sh log reverse module=ssh

Date/Time   S Mod  Type  SType Message  
-------------------------------------------------------------------------------
27 17:51:39 3 SSH  SSH   RJCT  Rejecting SSH connection from 43.29.53.43  
                               incompatible versions
27 17:43:23 3 SSH  SSH   RJCT  Rejecting SSH connection from 45.14.11.37  
                               incompatible versions
27 17:37:53 3 SSH  SSH   RJCT  Rejecting SSH connection from 43.29.53.51  
                               incompatible versions
</code></pre>

<p>この機能は AR5xxS系や8xxxXL系で使えるが、9424T/SP等では使えない。</p>

<hr>

<h4 id="securitymode">Security Mode の有効時間を伸ばす</h4>

<p>IPSec/PKI/SSH を有効化して SECURITYOFFICERユーザに切り替えると、デフォルトでは1分で再ログインを求められるようになる。いくらなんでもこれは短いと感じた時は、次のコマンドを打ち込む。</p>

<pre><code class="language-brush:plain">set user securedelay=3600  
</code></pre>

<p>設定可能な範囲は 10〜3600秒である。最短の10秒にすると相当の苦行を味わえるが、タイプ速度が上達する効能もある。</p>

<hr>

<h4 id="">非同期ポートのボーレートを変更する</h4>

<p>アラテレ製品では、管理用シリアルコンソールポートを<mark>非同期ポート</mark>と呼称する。初期値は baud=9600なのでこれを変更するには、次のようなコマンドを打ち込む。</p>

<pre><code class="language-brush:plain">set asyn=0 speed=115200

# 以下でも同じ（show conf dynの表示はこちら）
set asyn=asyn0 speed=115200  
</code></pre>

<p>打ち込んだ瞬間に設定が反映されるので、現在正に非同期ポートでログインしている場合は、対向端末側も設定を合わせるまで制御不能になることに注意しよう。</p>

<p>だがこの設定にかかわらず、機器ブート（起動時診断）メッセージは baud=9600固定である。従って動的にボーレートを変更できる端末ソフトを使っていないと思ったより難儀してしまう。結局できるだけ 9600固定で運用するほうがいざというとき慌てずに済むかもしれない。</p>

<p>なお設定内容は <code>show asyn=0</code> で確認できる。</p>

<hr>

<h4 id="ntp">NTPモジュール</h4>

<p>NTP時刻合わせのテンプレを以下に示す。ログ記録に関わるので、機器に IPを付したら真っ先に行う作業だ。</p>

<pre><code class="language-brush:plain"># NTPモジュールを活かして時差を指定する
enable ntp  
set ntp utc=+09:00:00

# peerアドレスは一つだけ書ける
# DNSモジュールを活かしていても FQDNでは指定できない。
add ntp peer=133.243.238.163

# 強制同期
# "ACTIVATE NTP" コマンドではない（そういうものはない）
reset ntp

# ステータス表示
show ntp

# peerアドレスの変更
# "SET NTP PEER"コマンドが存在しないので "DEL"して "ADD"する
# 当然DNSラウンドロビンも出来ないので機器毎にバラけた設定をしたりする
del ntp peer=133.243.238.163  
add ntp peer=133.243.238.164  
</code></pre>

<hr>

<h4 id="l3ppp">L3スイッチでの PPP接続</h4>

<p>L3スイッチでのLAN型PPP接続の例を挙げる。下記は 8948XL、8724SL/8748SLでの場合だ。<sup id="fnref:2"><a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fn:2" rel="footnote">2</a></sup></p>

<pre><code class="language-brush:plain"># Port1をアップリンクポートとして ONU接続に使う
create vlan="PPPoE-FLET" vid=4  
add vlan="4" port=1

# そのポートの中に PPPセッションを張って PPPインタフェースを作る
create ppp=4 over=vlan4-any  
set ppp=4 bap=off iprequest=on username="&lt;ISPアカウント&gt;" password="&lt;パスワード&gt;"  
set ppp=4 over=vlan4-any lqr=off echo=10 

# PPPインタフェースの IPは ISPから貰う
enable ip  
enable ip remote  
add ip int=ppp4 ip=0.0.0.0

# ・・・ことにしつつ、内側のVLANにそのIPを付けて NETMASKを切る
# これがL3スイッチ配下から見たゲートウェイIPになる
add ip int=vlan1 ip=&lt;付与されたグローバルIPのひとつ&gt; mask=255.255.255.248

# デフォルトルートは PPPインタフェースに丸投げする
add ip rou=0.0.0.0 mask=0.0.0.0 int=ppp4 next=0.0.0.0  
</code></pre>

<p>上位への接続は L3ルーティングとなる。設定のミソは本来 PPPインタフェースに付くべき IPを、内側の VLANにつけるところだ。</p>

<p>言うまでもなかろうが、IPが一つしか付与されない端末型接続の PPPでは、NATが使えなければおよそ意味が無い。L3スイッチ系では通常は NAT接続ができないので（ファイアウォールライセンスの購入が別途必須）、その場合はおとなしく ARシリーズを使おう。AR系と 89xx/87xx系の使い分けは、装備された物理ポートの数とNAT機能の有無で、PPPインタフェースの設定については概ね互換性がある。</p>

<hr>

<h4 id="">工場出荷時初期化</h4>

<p>Security Mode 状態から工場出荷時設定へ一気に戻すには次のようにする。AR550S v2.9.2 で確認した。</p>

<pre><code class="language-brush:plain"># 非同期ポート速度を初期値に変更
set asyn=0 speed=9600

# 設定ファイルを読み込まずにコールドスタート
restart router config=none

Warning: Config file MUST add a user with SECURITY OFFICER privilege.  
Do you wish to proceed with restart using specified config?(y/n)

INFO: Initialising Flash File System.  
INFO: Router startup complete

# 規定値ユーザの manager/friend でログイン
login: manager  
Password:

# SECURITY_MODE を切る
disable system security_mode

Warning: This command will disable security mode and delete all security files.  
Are you sure you wish to proceed?(y/n)  y

Info (1034003): Operation successful.

# 起動設定を消して、元の起動設定ファイルを rename あるいは delete fileする
set conf=none  
rename boot.cfg boot-old.cfg  
</code></pre>

<p>見れば解るが、デフォルトの managerユーザは Security Mode を切ることができる。起動設定ファイル（規定値では boot.cfg）を削除して再起動しても同じことだが、Security Mode 状態は設定ファイルと関係なく EEPROM内に保存されているため、これを切る方法を知らないと show confも create confも出来ない。</p>

<p>ところで起動診断時に以下のメッセージの途中で数秒間止まる。そこで "y" を押すと何が起きるだろう？</p>

<pre><code class="language-brush:plain">INFO: Self tests beginning.  
INFO: RAM test beginning.  
PASS: RAM test, 65536k bytes found.  
INFO: Self tests complete.  
INFO: Downloading router software.  
Force EPROM download (Y) ? 

INFO: Initialising Flash File System.  

INFO: Executing configuration script &lt;flash:boot-1.cfg&gt;  
INFO: Router startup complete

AR550S login:  
</code></pre>

<p>答えは「起動設定ファイルは読み込むが、すべてのEnableコマンドを無視する（組み込みモジュールを有効化しない）」である。このため Security Mode は切れて起動するものの、SET USERは通常通り実行されているために SECURITY OFFICERでログインすることが出来ず、かつ managerユーザが設定ファイルから削除もしくはパスワード変更されていたならば、結局は再ログイン手段が得られずに行き詰まる。</p>

<p>ところがここで "y" ではなく "s" を押すとどうなるだろう？<sup id="fnref:3"><a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fn:3" rel="footnote">3</a></sup></p>

<pre><code class="language-brush:plain">Force EPROM download (Y) ?  
INFO: Initial download successful.

INFO: Initialising Flash File System.  


INFO: Router startup complete

 login: 
</code></pre>

<p>ログインプロンプトに "SET SYS NAME" が反映されていなければ成功である。つまり起動設定ファイルを読み込まないで起動できる。こうなれば自動的に managerユーザが復活するので、それでログインして Security Modeを切り、工場出荷時状態に戻してしまえる。</p>

<p>この Undocumentedな挙動は、対応しているいくつかの機種・ファームウェアでのみ可能<sup id="fnref:4"><a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fn:4" rel="footnote">4</a></sup>ですべての機種で可能なわけではない。同じ機種でもファームウェアによっては（Undocumentedなのだから当然だが）そもそも対応していなかったり、GS916M/GS924M に至っては managerユーザは復活するもののパスワードはリセットされない<sup id="fnref:5"><a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fn:5" rel="footnote">5</a></sup>ので、パスワード忘れ事案には対応できない。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>CentreCOMは開発担当部署によっていくつかの製品グループがある。AR5xxSルータと89xxXL/83xxXLは同族だが、87xxSLはそれより1世代古くて多少性格が異なり、94xxTSと GS9xxMはいずれも全く異なる出自と設計なので似て非なる挙動を示したりする。これらの見分け方というか個性の違いは、見たとおり型番ルールの類似性や（アンドキュメントな）隠し機能の有無、コンソールエラーメッセージの違いに強く現れる。 <a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>8948XLは正式には PPPに対応しておらず Undocumentedなのだが、実際には 8724SL/8748SLと同様の設定と運用ができる。両者の決定的な違いはファイアウォール（NAT機能）追加ライセンスが設定されているか否かだ。だが NAT設定が不要＝LAN型接続であれば、8948XLでも不都合なく設定して運用できる。 <a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>恐らく SKIP の頭文字だ。必ずしもすべてのファームウェアで対応している保証はない。別の一部機種では起動メッセージ中で明示的に "S"と表示しているものもあるが、AR5xxS系の場合はノーヒントである。 <a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>アラテレ製品の場合、パスワード忘れによる工場出荷時初期化は、有償修理対応が原則である。エンドユーザレベルでこれが出来るのは一部機種の隠し機能に限られる。 <a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p>GS916M/GS924Mは、パスワードが EEPROMに独立して記録されるため設定ファイルを読み飛ばしても再ログイン可能にはならない。だがこの機種はブートローダー起動中に特定の操作をすると、ブートローダーシェルに入ることができる。そこで EEPROM内容のダンプ＆エディットが不可能でもない。 <a href="http://multix.jp/allied-telesis-centrecom-memorandom/#fnref:5" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[ヘッドレス用memtest86+の作成]]></title><description><![CDATA[<p>memtest86+はふつうは公式サイト<sup id="fnref:1"><a href="http://multix.jp/buildup-memtest-headless/#fn:1" rel="footnote">1</a></sup>等から出来合いのバイナリを入手してくれば充分だが、通常は VGA出力必須のためシリアルコンソールでのヘッドレス環境では実行できない。しかしソースコードには必要最小限ではあるがシリアルコンソール対応コードが含まれているため、ヘッダを修正して makeしなおせばヘッドレス環境で使えるmemtestバイナリを手に入れることができる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">注意点</h4>

<p>memtestのシリアルコンソール対応は、既に充分にはメンテナンスされていないのか若干の不具合がある。例えば configurationメニューから抜けたあと画面が乱れるので起動後のテストオプションが事実上変更できないとか、ESCキーで終了したとき rebootではなく system haltで止まってしまうなどだ。とは言え VGAデバイスを持たない機材のメモリ健全性テストができるだけで充分な利用価値があったりする。</p>

<p>なお memtestバイナリは 32bit実行ファイルとなるので、64bit環境で buildするには <code>/usr/include/gnu/stubs-32.h</code>が要求される。</p>

<hr>

<h4 id="centos">CentOSの場合</h4>

<p>まず必要なパッケージをインストールする。基本的な gccやライブラリの類は <code>yum groupinstall "Development Tools"</code>で揃う。また memtestを makeするために 32bitバージョンの glibc-develもインストールする。</p>

<pre><code class="language-brush:bash">sudo yum groupinstall -y "Development</code></pre>]]></description><link>http://multix.jp/buildup-memtest-headless/</link><guid isPermaLink="false">dadf31ad-7b3a-4598-85e2-3dab8a594530</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Thu, 02 Jul 2015 05:54:24 GMT</pubDate><content:encoded><![CDATA[<p>memtest86+はふつうは公式サイト<sup id="fnref:1"><a href="http://multix.jp/buildup-memtest-headless/#fn:1" rel="footnote">1</a></sup>等から出来合いのバイナリを入手してくれば充分だが、通常は VGA出力必須のためシリアルコンソールでのヘッドレス環境では実行できない。しかしソースコードには必要最小限ではあるがシリアルコンソール対応コードが含まれているため、ヘッダを修正して makeしなおせばヘッドレス環境で使えるmemtestバイナリを手に入れることができる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">注意点</h4>

<p>memtestのシリアルコンソール対応は、既に充分にはメンテナンスされていないのか若干の不具合がある。例えば configurationメニューから抜けたあと画面が乱れるので起動後のテストオプションが事実上変更できないとか、ESCキーで終了したとき rebootではなく system haltで止まってしまうなどだ。とは言え VGAデバイスを持たない機材のメモリ健全性テストができるだけで充分な利用価値があったりする。</p>

<p>なお memtestバイナリは 32bit実行ファイルとなるので、64bit環境で buildするには <code>/usr/include/gnu/stubs-32.h</code>が要求される。</p>

<hr>

<h4 id="centos">CentOSの場合</h4>

<p>まず必要なパッケージをインストールする。基本的な gccやライブラリの類は <code>yum groupinstall "Development Tools"</code>で揃う。また memtestを makeするために 32bitバージョンの glibc-develもインストールする。</p>

<pre><code class="language-brush:bash">sudo yum groupinstall -y "Development Tools"

# 64bit環境の場合
sudo yum install -y glibc-devel-2.17-78.el7.i686  
</code></pre>

<p>memtestの SRPMをダウンロードし、rpmコマンドで展開する。展開結果は <code>~/rpmbuild</code>ディレクトリ以下に現れる。</p>

<pre><code class="language-brush:bash">rpm -iv ftp://ftp.pbone.net/mirror/vault.centos.org/7.1.1503/os/Source/SPackages/memtest86+-4.20-14.el7.src.rpm  
</code></pre>

<p>シリアルポートの設定は、memtestの condfig.hで行うので、これに対する patchファイルを用意する。<strong>SERIAL_CONSOLE_DEFAULT を1にするだけ</strong>だが、これによって起動オプションとして <code>console=CONFIGURATION</code>が記述できるようになる。</p>

<pre><code class="language-brush:plain gutter:true highlight:[8] title:~/rpmbuild/SOURCES/memtest86+-4.20-serial.patch">--- memtest86+-4.20/config.h        2011-01-24 03:11:04.000000000 +0900
+++ memtest86+-4.20/config.h.serial 2015-07-03 13:15:26.716441211 +0900
@@ -13,7 +13,7 @@
 /* SERIAL_CONSOLE_DEFAULT -  The default state of the serial console. */
 /*     This is normally off since it slows down testing.  Change to a 1 */
 /*     to enable. */
-#define SERIAL_CONSOLE_DEFAULT 0
+#define SERIAL_CONSOLE_DEFAULT 1

 /* SERIAL_TTY - The default serial port to use. 0=ttyS0, 1=ttyS1 */ 
 #define SERIAL_TTY 0
</code></pre>

<p>次いで specファイルを修正する。元の memtest86+.specをコピーして、これらを適切な場所に追加する。</p>

<pre><code class="language-brush:bash">cp ~/rpmbuild/SPECS/memtest86+.spec ~/rpmbuild/SPECS/memtest86+-serial.spec  
</code></pre>

<pre><code class="language-brush:bash title:~/rpmbuild/SPECS/memtest86+-serial.spec fix"># Patch1指示を Patch0指示行の直後に追加
Patch1:   memtest86+-4.20-serial.patch

# %patch1コマンドを %patch0コマンド行の直後に追加
%patch1 -p1 -b .serial
</code></pre>

<p>あとは <code>rpmbuild</code>コマンドを叩けば、RPMパッケージが作成される。これをインストールすると /bootに展開されるが、LiveBootの場合必要なのは memtestバイナリ本体だけなので（LiveBoot環境で buildした場合は）出来上がった memtest.binを <code>/run/initramfs/live/syslinux/mt86plus</code>にコピーして、syslinux.cfgを編集すれば事足りる。<sup id="fnref:2"><a href="http://multix.jp/buildup-memtest-headless/#fn:2" rel="footnote">2</a></sup></p>

<pre><code class="language-brush:bash highlight:[4,7]">rpmbuild -bb rpmbuild/SPECS/memtest86+-serial.spec

ls -l ~/rpmbuild/RPMS/x86_64/memtest86+-4.20-14.el7.centos.x86_64.rpm  
-rw-rw-r-- 1 liveuser liveuser 80616 Jul  2 18:31 rpmbuild/RPMS/x86_64/memtest86+-4.20-14.el7.centos.x86_64.rpm

ls -l ~/rpmbuild/BUILD/memtest86+-4.20/memtest.bin  
-rwxr-xr-x 1 liveuser liveuser 176500 Jul  2 18:31 rpmbuild/BUILD/memtest86+-4.20/memtest.bin

cp ~/rpmbuild/BUILD/memtest86+-4.20/memtest.bin /run/initramfs/live/syslinux/mt86plus  
</code></pre>

<pre><code class="language-brush:plain title:/run/initramfs/live/syslinux/syslinux.cfg fix"># Troubleshootingメニューに追加
label memtest  
  menu label Test ^memory
  kernel mt86plus
  append console=ttyS0,115200n8
</code></pre>

<p>memtest.binは FDDブートイメージなので、FDDへはそのまま ddするだけでも使える。ただしそれでは今回のようなシリアルコンソール設定オプションを渡すすべがないので、syslinux等の汎用ブートローダーを利用して起動するほうが一般的だろう。</p>

<hr>

<h4 id="ubuntu">Ubuntuの場合</h4>

<p>まず build環境を整える。<code>apt-get build-dep PACKAGE</code>で指定パッケージを buildするのに必要なツール類がまとめてインストールされる。</p>

<pre><code class="language-brush:bash">sudo apt-get update  
sudo apt-get install devscripts  
sudo apt-get build-dep memtest86+  
apt-get source memtest86+  
</code></pre>

<p>memtestのシリアルポート設定は CentOSの場合と同様だが（memtest86+-4.20/debian/rulesを修正するのも面倒なので）config.hは直接修正する。その後はソースディレクトリの中で <code>debuild -us -uc</code>を実行すれば memtest.binバイナリと、親ディレクトリに .debファイルが作成される。</p>

<pre><code class="language-brush:bash highlight:[8]">cd memtest86+-4.20/

vi config.h

debuild -us -uc

ls -l memtest.bin  
-rwxr-xr-x 1 ubuntu ubuntu 164216 Jul  3 02:57 memtest.bin
</code></pre>

<p>memtest.binは LiveBootフラッシュドライブの mt86plusと差し替えて使用する。</p>

<pre><code class="language-brush:bash highlight:[2]">ls -l /cdrom/install/mt86plus  
-rwxr-xr-x 1 root root 150024 Jun 22 16:11 /cdrom/install/mt86plus

cp memtest.bin /cdrom/install/mt86plus  
</code></pre>

<pre><code class="language-brush:plain title:isolinux/txt.cfg fix">label memtest  
  menu label Test ^memory
  kernel /install/mt86plus
  append console=ttyS0,115200n8
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p><a href="http://www.memtest.org">http://www.memtest.org</a> <a href="http://multix.jp/buildup-memtest-headless/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>syslinuxのバージョンにもよるが kernel/appendコマンドに渡すファイル名に拡張子が付いていると正常認識されないことがあるので、CentOSでは慣習的に8文字以下の拡張子なしファイル名にすることが多い。 <a href="http://multix.jp/buildup-memtest-headless/#fnref:2" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[LinuxヘッドレスLiveBoot（CentOS編）]]></title><description><![CDATA[<p>本稿は<a href="http://multix.jp/headless-liveboot-ubuntu/">前稿</a>の CentOS版だ。要件定義はおよそ同じだが、LiveBootの実現方法の違いにより、構築手順は大きく異なる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する CentOSはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされているものとする。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-centos/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物は ISOイメージとし、CD-R/DVD-Rに焼いて USB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成の USBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>yum一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>liveuser</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的な</li></ol>]]></description><link>http://multix.jp/headless-liveboot-centos/</link><guid isPermaLink="false">cdc0b430-1ee4-4d3b-b6b8-e9407ad9c019</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[Linux]]></category><category><![CDATA[めもらんだむ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Wed, 24 Jun 2015 02:31:27 GMT</pubDate><content:encoded><![CDATA[<p>本稿は<a href="http://multix.jp/headless-liveboot-ubuntu/">前稿</a>の CentOS版だ。要件定義はおよそ同じだが、LiveBootの実現方法の違いにより、構築手順は大きく異なる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する CentOSはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされているものとする。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-centos/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物は ISOイメージとし、CD-R/DVD-Rに焼いて USB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成の USBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>yum一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>liveuser</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的な LiveBootの流儀）  </li>
<li>既定のネットワーク設定・接続はオフとする。  </li>
<li>これらのパスワードやネットワーク設定は persistent有効時には自由に設定変更して永続化することができ、再起動しても設定内容が失われてはならない。</li>
</ol>

<p>以後は CentOS-7-x86_64-LiveCD-1503.iso をベースにして話を進める。32bit版や CentOS-6でも同様の手順で構築できる。<sup id="fnref:2"><a href="http://multix.jp/headless-liveboot-centos/#fn:2" rel="footnote">2</a></sup></p>

<p>Ubuntuでは最終的な rootfsを作るのにオンメモリ（2GiB程度）で作業できたが、CentOS用の rootfsを作成するには作業領域が足りないため、起動用 USBメモリの他に Ext4でフォーマットできるフラッシュドライブや HDDを別途用意しなければならない。なお最終的に出来上がるブータブルイメージは CD-Rに充分格納できるサイズ（640MiB以下）になる。</p>

<p>なお製作時のリファレンス機材には Riava RS670A を使用した。<sup id="fnref:3"><a href="http://multix.jp/headless-liveboot-centos/#fn:3" rel="footnote">3</a></sup></p>

<hr>

<h4 id="usb">シリアルコンソール対応のインストーラUSBメモリを作る</h4>

<p>まずヘッドレス環境で使用できる CentOSインストーラを LiveCD ISOイメージから作成する。Windows上でこれを行うには Fedora LiveUSB Creator<sup id="fnref:4"><a href="http://multix.jp/headless-liveboot-centos/#fn:4" rel="footnote">4</a></sup>を使用する。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-24-11-56-53.png" alt=""></p>

<p>次に起動メニューを書き換える。LiveUSB Creatorが作成したベースファイルからの相違点はシリアルコンソール設定の追加が主だ。</p>

<pre><code class="language-brush:plain gutter:true title:/syslinux/syslinux.cfg （フラッシュドライブ起動用）">#serial 0 115200
#console 0

default vesamenu.c32  
timeout 100  
menu background  
menu autoboot Starting CentOS Linux 7 in # second{,s}. Press any key to interrupt.

menu clear  
menu title CentOS Linux 7  
menu vshift 4  
menu rows 12  
menu margin 8  
menu helpmsgrow 15  
menu tabmsgrow 13

menu tabmsg Press Tab for full configuration options on menu items.  
menu separator  
menu separator  
label linux0  
  menu label ^Start CentOS Linux 7 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=LIVE rootfstype=vfat ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 console=tty0 console=ttyS0,115200n8
  menu default
menu separator  
menu begin ^Troubleshooting  
  menu title Troubleshooting
label check0  
  menu label ^Test this media &amp; start CentOS Linux 7 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=LIVE rootfstype=vfat ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 rd.live.check console=tty0 console=ttyS0,115200n8
menu separator  
label local  
  menu label Boot from ^local drive
  localboot 0xffff
menu separator  
label returntomain  
  menu label Return to ^main menu.
  menu exit
menu end  
</code></pre>

<p>このフラッシュドライブで、Troubleshootingサブメニュー内の <strong>Test this media &amp; start CentOS Linux 7 Live</strong> を選んで起動しよう。しばらく待っているとログインプロンプトが出現する。ここからは <code>root[ENTER]</code>で rootシェルに入ることが出来る。</p>

<hr>

<h4 id="rootfs">rootfsを作成する</h4>

<p>この時点で NetworkManager は起動しているので DHCPでのIP取得は既に済んでいるだろう。作業に必要なパッケージを yumで追加する。</p>

<pre><code class="language-brush:bash">LANG=C  
yum install -y squashfs-tools  
</code></pre>

<p>作業用の追加ドライブを接続してパーテションをフォーマットし、<code>/mnt</code>にマウントしてそこに移動する。ここでは <code>/dev/sdb1</code>とする。</p>

<pre><code class="language-brush:bash">mkfs.ext4 /dev/sdb1  
mount /dev/sdb1 /mnt  
cd /mnt  
</code></pre>

<p>起動時のフラッシュドライブは <code>/run/initramfs/live</code>に ReadOnlyでマウントされているので、これを rwオプションでリマウントして書き換え可能にする。</p>

<pre><code class="language-brush:bash">mount -o rw,remount /run/initramfs/live  
cd /run/initramfs/live  
</code></pre>

<p>現在起動している rootfsの実体は <code>/run/initramfs/live/LiveOS/squashfs.img</code>だ。このファイルを差し替えるのが最終目的だが、これは <code>LiveOS/ext3fs.img</code>という loopbackファイルを squashfsで圧縮した構造になっている。そこでこれを以下のようにして再現する。rootfs容量はここでは8GiBとするが、適宜任意のサイズに変えても良い。</p>

<pre><code class="language-brush:bash">mkdir -p squashfs-root/LiveOS  
truncate -s $((2**33)) squashfs-root/LiveOS/ext3fs.img  
mkfs.ext4 -L _CentOS-7-livecd squashfs-root/LiveOS/ext3fs.img  
</code></pre>

<p>このファイルを loopbackマウントして /mnt/chrootでアクセスできるようにする。</p>

<pre><code class="language-brush:bash">mkdir /mnt/chroot  
mount -o loop squashfs-root/LiveOS/ext3fs.img /mnt/chroot/  
</code></pre>

<p>後は yumのグループインストールコマンド一発で、任意の新しい rootfs環境が構築できる。</p>

<pre><code class="language-brush:bash">yum --installroot=/mnt/chroot --releasever=7 -y groupinstall Core  
</code></pre>

<p>releaseverオプションに指定する CentOSのバージョンは、<code>/etc/os-release</code>ファイルを参照すると良い。</p>

<hr>

<h4 id="rootfs">rootfsの環境設定</h4>

<p>まず各システム領域を chroot内に bindマウントする。そしてダミーの <code>/etc/fstab</code>（中身は空でも良い）を作成しておく。</p>

<pre><code class="language-brush:bash">mount -o bind /dev/ chroot/dev/  
mount -o bind /proc/ chroot/proc/  
mount -o bind /tmp/ chroot/tmp/  
mount -o bind /run/ chroot/run/  
touch chroot/etc/fstab  
</code></pre>

<p>initramfsの構築設定ファイルを追加する。これは LiveBoot可能な initramfsを作成するのに必須の設定だ。</p>

<pre><code class="language-brush:plain title:chroot/etc/dracut.conf.d/01-liveos.conf">hostonly="no"  
add_dracutmodules+="dmsquash-live"  
compress="xz"  
</code></pre>

<p>現在起動している LiveCDシステムの、livesysサービスファイルを chroot内にコピーしてサービス登録する。このサービスは初回起動時に <em>liveuser</em>アカウントの作成や rootパスワードの削除処理を行うようになっている。<sup id="fnref:5"><a href="http://multix.jp/headless-liveboot-centos/#fn:5" rel="footnote">5</a></sup></p>

<pre><code class="language-brush:bash">cp -p /etc/init.d/livesys* chroot/etc/init.d/  
chroot chroot/ chkconfig --add livesys  
chroot chroot/ chkconfig --add livesys-late  
</code></pre>

<p>タイムゾーンとロケール情報を設定し、NetworkManagerが初回起動時は起動しないように修正する。</p>

<pre><code class="language-brush:bash">ln -sf ../usr/share/zoneinfo/Asia/Tokyo chroot/etc/localtime

echo "LANG=en_US.utf8" &gt; chroot/etc/locale.conf

chroot chroot /usr/bin/systemctl disable NetworkManager  
</code></pre>

<hr>

<h4 id="linuxkernel">Linux kernelモジュールの組み込み</h4>

<p>kernelと、その他の追加パッケージをインストールする。</p>

<pre><code class="language-brush:bash">yum --installroot=/mnt/chroot -y install kernel squashfs-tools dump isomd5sum epel-release  
</code></pre>

<p>kernelと作成された initramfsを、フラッシュドライブに移動する。rescue関係のファイルは必要ないので削除する。</p>

<pre><code class="language-brush:bash">rm -f chroot/boot/*rescue*  
mv chroot/boot/vmlinuz-* /run/initramfs/live/syslinux/vmlinuz0.new  
mv chroot/boot/initramfs-* /run/initramfs/live/syslinux/initrd0.img.new  
</code></pre>

<p>最後にクリーンアップを行ってアンマウントする。rootfsの umount後は fsckで損傷がないかチェックする。</p>

<pre><code class="language-brush:bash highlight:[9]">yum --installroot=/mnt/chroot clean all  
find chroot/var/log -type f -exec /bin/truncate -s0 {} \;  
umount chroot/proc/  
umount chroot/dev/  
umount chroot/run/  
umount chroot/tmp/

du -sh chroot/  
720M    chroot/

umount chroot/  
fsck.ext4 squashfs-root/LiveOS/ext3fs.img  
</code></pre>

<hr>

<h4 id="squashfsimg">squashfs.img ファイルの作成</h4>

<p>こうして出来た rootfsを mksquashfsで圧縮すれば、LiveBoot可能な新しいシステムファイルになる。ただし Ext4フォーマットした loopbackファイルをそのまま圧縮するため、インストール過程で生じた削除ファイルの痕跡などもそのまま残っていて圧縮率が低下してしまう。</p>

<pre><code class="language-brush:bash highlight:[4]">mksquashfs squashfs-root squashfs.img.new

ls -lh squashfs.img.new  
-rw-r--r--. 1 root root 474M Jun 24 08:21 squashfs.img.new
</code></pre>

<p>これを可能な限り取り除いて圧縮するには、dump/restoreコマンドを経由してファイルシステムを完全に作り直すとよい。ただし dumpコマンドはブロックデバイスしか扱えないので、losetupコマンドを用いて /dev/loopX に loopbackファイルをバインドして指定する。手順は面倒だが、比較するとその効果は絶大だ。</p>

<pre><code class="language-brush:bash highlight:[11,26,27]">mv squashfs.img.new squashfs.img.nev

yum install -y dump

loopdev=$(losetup -f)  
losetup $loopdev squashfs-root/LiveOS/ext3fs.img  
dump -0z9 -f rootdump $loopdev  
losetup -d $loopdev

ls -lh rootdump  
-rw-r--r--. 1 root root 277M Jun 24 08:30 rootdump

rm -f squashfs-root/LiveOS/ext3fs.img  
truncate -s $((2**33)) squashfs-root/LiveOS/ext3fs.img  
mkfs.ext4 -L _CentOS-7-livecd squashfs-root/LiveOS/ext3fs.img

mount -o loop squashfs-root/LiveOS/ext3fs.img chroot  
(cd chroot/; restore -r -f ../rootdump)
rm -fr chroot/lost+found restoresymtable  
umount chroot

fsck.ext4 squashfs-root/LiveOS/ext3fs.img  
mksquashfs squashfs-root squashfs.img.new

ls -lh squashfs.img.ne?  
-rw-r--r--. 1 root root 474M Jun 24 08:21 squashfs.img.nev
-rw-r--r--. 1 root root 250M Jun 24 08:38 squashfs.img.new

cp squashfs.img.new /run/initramfs/live/LiveOS/  
</code></pre>

<hr>

<h4 id="rootfs">新 rootfsでの起動テスト</h4>

<p>こうして作成した rootfsファイルをブートUSBメモリにコピーして shutdownし、Windows（あるいは他のLinux機）で以下の起動用ファイルを差し替える。</p>

<ul>
<li>vmlinuz0.new を syslinux/vmlinuz0 へ</li>
<li>initrd0.img.new を syslinux/initrd0.img へ</li>
<li>squashfs.img.new を LiveOS/squashfs.img へ</li>
</ul>

<p>syslinux/syslinux.cfg は前述のそのままでよいが、今度の起動は通常の <strong>Start CentOS Linux</strong>ラベルを選択する。ログイン・プロンプトは <em>root</em>で直接、あるいは <em>liveuser</em>（いずれもパスワード無し）でシェルに入れる。<em>liveuser</em>アカウントは sudoで <em>root</em>アカウントになれる。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-24-16-47-35.png" alt=""></p>

<p>NetworkManagerは停止状態なので、ネットワークに接続するには dhclientで Link Upするか、nmcli/nmtuiでインタフェース設定を行って NetworkManagerを起動する。</p>

<pre><code class="language-brush:bash">sudo -s

systemctl enable NetworkManager  
systemctl start NetworkManager

nmcli connection modify enp2s0 ipv4.method manual ipv4.addresses 192.168.240.101/24 ipv4.gateway 192.168.240.1 ipv4.dns 8.8.8.8

systemctl restart network.service

# cat /etc/sysconfig/network-scripts/ifcfg-enp2s0
</code></pre>

<hr>

<h4 id="iso">ISOイメージを作成する</h4>

<p>元の ISOの squashfs.img等を差し替えた、新しいISOイメージを作成する。まず作業に必要なパッケージをインストールし、作業領域に ISO内のファイルを展開して ReadOnlyを外す。</p>

<pre><code class="language-brush:bash">mount /dev/sdb1 /mnt  
cd /mnt

# install mkisofs tools
yum install -y mkisofs isomd5sum

# mount image: CentOS-7-x86_64-LiveCD-1503.iso
mkdir loop  
mount /dev/cdrom loop

# copy iso files and modify attributes
cp -rf loop files  
chmod -R +w files/  
umount loop  
</code></pre>

<p>フラッシュドライブから先に作成した新しいシステムファイルをコピーする。このときフラッシュドライブ上では <code>/syslinux</code>に vmlinuz0と initrd0.img ファイルが置かれているが、ISOイメージでは <code>/isolinux</code>ディレクトリになる。<sup id="fnref:6"><a href="http://multix.jp/headless-liveboot-centos/#fn:6" rel="footnote">6</a></sup></p>

<pre><code class="language-brush:bash"># copy kernel and filesystem
cp -f /run/initramfs/live/syslinux/vmlinuz0 files/isolinux/  
cp -f /run/initramfs/live/syslinux/initrd0.img files/isolinux/  
cp -f /run/initramfs/live/LiveOS/squashfs.img files/LiveOS/  
</code></pre>

<p>files/isolinux/isolinux.cfg をシリアルコンソール対応に修正する。最初のシリアルポート設定は BIOSコンソールリダイレクトの有無による。また先に作成した rootfsは SELinuxをサポートするように構築してはいないので、これを無効にする。<code>isolinux/mt86plus</code>については標準では存在しないが、これについては<a href="http://multix.jp/buildup-memtest-headless/">別稿を参照</a>されたい。</p>

<pre><code class="language-brush:plain gutter:true title:isolinux/isolinux.cfg （ODD起動用）">#serial 0 115200
#console 0

default vesamenu.c32  
timeout 100  
menu background  
menu autoboot Starting CentOS Linux 7 in # second{,s}. Press any key to interrupt.

menu clear  
menu title CentOS Linux 7  
menu vshift 4  
menu rows 12  
menu margin 8  
#menu hidden
menu helpmsgrow 15  
menu tabmsgrow 13

menu tabmsg Press Tab for full configuration options on menu items.  
menu separator  
menu separator  
label linux0  
  menu label ^Start CentOS Linux 7 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=CentOS7-liveboot rootfstype=auto ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 selinux=0 console=tty0 console=ttyS0,115200n8
  menu default
menu separator  
menu begin ^Troubleshooting  
  menu title Troubleshooting
label check0  
  menu label ^Test this media &amp; start CentOS Linux 7 1503 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=CentOS7-liveboot rootfstype=auto ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 rd.live.check selinux=0 console=tty0 console=ttyS0,115200n8 
label memtest  
  menu label Test ^memory
  kernel mt86plus
  append console=ttyS0,115200n8
menu separator  
label local  
  menu label Boot from ^local drive
  localboot 0xffff
menu separator  
label returntomain  
  menu label Return to ^main menu.
  menu exit
</code></pre>

<p>mkisofsコマンドで ISOイメージを作成する。ここではもっともシンプルなオプション構成として GRUB（gfxboot）や EFI対応、Macintosh対応は行ってはいない。注意が必要なのは -Vオプションに渡すCDラベルで、これは kernel起動オプションに記述したのと同じものでなければならない。またこのラベルは最大16文字なのでこれを超えないようにしよう。</p>

<pre><code class="language-brush:bash">find ./files -print | xargs touch -m {} \;  
LANG=C mkisofs -r -J -l \  
  -V CentOS7-liveboot \
  -cache-inodes \
  -b isolinux/isolinux.bin \n
  -c isolinux/boot.cat \
  -no-emul-boot \
  -boot-load-size 4 \
  -boot-info-table \
  -o CentOS7-liveboot.iso \
  ./files
</code></pre>

<p>こうして出来た ISOイメージに、それ自体の改竄検出 MD5チェックサムを implantisomd5コマンドで埋め込む。Ubuntuではファイル単位で MD5をチェックしていたが、CentOSではメディアそのものをチェックする。従ってフラッシュドライブではこの仕組は動かない。一方で CD-R等に焼いた際に Write Errorが有っても、メディアの完全性を確認することが出来る。<sup id="fnref:7"><a href="http://multix.jp/headless-liveboot-centos/#fn:7" rel="footnote">7</a></sup></p>

<pre><code class="language-brush:bash">LANG=C implantisomd5 CentOS7-liveboot.iso  
</code></pre>

<p>改竄チェックは checkisomd5コマンドで行える。引数には調べたい ISOファイルか、メディアを挿れた ODDのデバイスファイルを指定すればよい。なお implantisomd5と checkisomd5コマンドは、isomd5sumパッケージに含まれている。<sup id="fnref:8"><a href="http://multix.jp/headless-liveboot-centos/#fn:8" rel="footnote">8</a></sup></p>

<pre><code class="language-brush:bash">checkisomd5 CentOS7-liveboot.iso  
checkisomd5 /dev/cdrom  
</code></pre>

<hr>

<h4 id="cdrwdvdrwiso">CD-RW/DVD-RWに ISOイメージを焼く</h4>

<p>光学メディアへのライティングは、Ubuntuとおなじく wodimコマンドで行える。</p>

<pre><code class="language-brush:bash">yum install -y wodim

# erase CD/DVD-RW media
wodim dev=/dev/sr0 blank=fast

# writing image
wodim -sao -eject dev=/dev/sr0 CentOS7-liveboot.iso

# check image
checkisomd5 /dev/sr0  
</code></pre>

<p>こうして作成した光学メディアで ODDブートが正常に行えたら、<em>Test this media &amp; start CentOS</em>ラベルで起動してみよう。検査が正常に終了するとログイン・プロンプトに進み、失敗した場合は System Haltするはずだ。<sup id="fnref:9"><a href="http://multix.jp/headless-liveboot-centos/#fn:9" rel="footnote">9</a></sup></p>

<hr>

<h4 id="persistent">persistentモードを設定する</h4>

<p>前述の ISOイメージ/光学メディアで ODDブートが正常に行えたなら、次はその ISOイメージを Live USB Creatorでもって、今度は persistent領域を設定したフラッシュドライブを作成しよう。</p>

<p>persistentモードは kernelオプションの <code>rd.live.overlay=LABEL=LIVE</code>によって有効になる。initramfsは指定された LABELを blkidコマンドで探し、これを <code>/run/initramfs/live/</code>にマウントする。persistent領域ファイルは <code>LiveOS</code>ディレクトリ内の <code>overlay-${LABEL}-${UUID}</code>というファイル名で探して、Device Mapperによって snapshotとして <code>/</code>に結合される。これ自体は初期状態では（ファイルシステムではないので）単なる Zero Fillファイルである。</p>

<pre><code class="language-brush:bash highlight:[2,5,8,9,10,11]">blkid -L LIVE  
/dev/sda1

blkid /dev/sda1  
/dev/sda1: LABEL="LIVE" UUID="0621-6D9B" TYPE="vfat" 

ls -l /run/initramfs/live/LiveOS/  
total 1299100  
-rwxr-xr-x 1 root root      20480 Jun 26 05:26 osmin.img
-rwxr-xr-x 1 root root 1068498944 Jun 30 12:32 overlay-LIVE-0621-6D9B
-rwxr-xr-x 1 root root  261758976 Jun 30 08:26 squashfs.img
</code></pre>

<p>Ubuntuと異なり、Device Mapperを使用しているためもあって実際にあとどの程度データが書き込めるか（保存できるか）は dfコマンドでは判断することができない。dmsetupコマンドの statusサブコマンドで、live-rwボリュームの snapshot消費ブロック数（512byte単位）を知ることはできるから、これを利用するしかない。</p>

<pre><code class="language-brush:bash highlight:[2,5]">dmsetup status live-rw  
0 16777216 snapshot 14960/2086912 72

echo | awk '{print 14960/2086912}'  
0.00716849  
</code></pre>

<p>また Ubuntuの場合は、persistentファイルは overlayfsで重ねられた独立したファイルシステムであったから、非起動状態のフラッシュドライブから persistent領域内のファイルを抜き出すことは容易だったが、CentOSではこれは容易なことではない。</p>

<p>（現在起動しているのとは別の）フラッシュドライブ内の persistent領域にアクセスするには、以下のようにする。</p>

<pre><code class="language-brush:bash highlight:[15,23,28]">## 作業領域の準備

cd /mnt  
mkdir media squash rootfs

## フラッシュドライブを作業領域にマウントし、その中の squashfs.imgをマウントする

mount /dev/sdc1 media  
mount -o loop media/LiveOS/squashfs.img squash

## ext3fs.img（readonly）と overlayファイルに loopbackデバイスを割り当てる
## 空きデバイス名は losetup -fで得られる

losetup -f  
/dev/loop6

losetup /dev/loop6 squash/LiveOS/ext3fs.img -r  
losetup /dev/loop7 edia/LiveOS/overlay-LIVE-0621-6D9B

## ふたつのデバイスを束ねて rootfsを再生する

blockdev -q --getsz /dev/loop6  
16777216

dmsetup create live-rootfs --table "0 16777216 snapshot /dev/loop6 /dev/loop7 P 8"

dmsetup status live-rootfs  
0 16777216 snapshot 17312/2086912 80

## あとはfsckしたり・・・

fsck -f -y /dev/mapper/live-rootfs

## dumpしたり・・・
## （squashfs.img作成以後分だけを抜き出すのに /etc/dumpdatesを利用する例）

echo "/dev/mapper/live-rootfs 0 $(LANG=C \  
  date +"%a %b %d %X %z" \
    -r media/syslinux/ldlinux.sys)" &gt;&gt; /etc/dumpdates
dump -u1z9 /dev/mapper/live-rootfs -f rootfsdump

## mountしたり・・・

mount /dev/mapper/live-rootfs rootfs  
ls -a rootfs/home/liveuser

## 作業を終えたら後始末してフラッシュドライブを切り離す

umount rootfs  
dmsetup remove live-rootfs  
losetup -d /dev/loop6  
losetup -d /dev/loop7  
umount squash  
umount media  
rmdir media squash rootfs  
</code></pre>

<hr>

<h4 id="homeswap">/homeや swapパーテションを設定する</h4>

<p><code>/etc/init.d/livesys</code>を覗いてみると解る<sup id="fnref:10"><a href="http://multix.jp/headless-liveboot-centos/#fn:10" rel="footnote">10</a></sup>が、フラッシュドライブの <code>/LiveOS</code>ディレクトリに、<code>home.img</code>ファイルがあれば <code>/home</code>に、swap.imgファイルが有れば <code>swap</code>に、それぞれ自動的にマウントするようになっている。これらのボリュームファイルは以下のようにセットアップする。</p>

<pre><code class="language-brush:bash highlight:[22,23,24,25,26,27,28,32,33,34]">## persistentオフの場合はフラッシュドライブを rwリマウントする 
mount -o rw,remount /run/initramfs/live

## 保存領域へcd
cd /run/initramfs/live/LiveOS

## home.img ボリュームの作成（サイズは適宜）
truncate -s $((2**31)) home.img  
mkfs.ext4 -L "home-vol" home.img

## swap.img ボリュームの作成（サイズは適宜）
truncate -s $((2**31)) swap.img  
mkswap -L "swap-vol" swap.img

## あとは特に設定変更もなく再起動するだけ
sync;sync;sync  
reboot

## homeボリュームの有無は losetupで確認できる
## 容量は dfコマンドで把握可能
losetup  
NAME       SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE  
/dev/loop0         0      0         0  1 /osmin.img (deleted)
/dev/loop1         0      0         0  1 /osmin
/dev/loop2         0      0         0  1 /run/initramfs/live/LiveOS/squashfs.img
/dev/loop3         0      0         0  1 /LiveOS/ext3fs.img
/dev/loop4         0      0         0  0 /LiveOS/overlay-LIVE-748B-70C6
/dev/loop5         0      0         0  0 /run/initramfs/live/LiveOS/home.img

## swapは freeコマンドで容量が確認できる
free  
              total        used        free      shared  buff/cache   available
Mem:        8162140      178452     7610852       16720      372836     7767880  
Swap:        262136           0      262136
</code></pre>

<hr>

<h4 id="">ダウンロード</h4>

<p><a href="https://secure.multix.jp/download/SerialLive/CentOS7-liveboot-20150703.iso">CentOS7-liveboot-20150703.iso</a> 313MiB (<a href="https://secure.multix.jp/download/SerialLive/CentOS7-liveboot-20150703.iso.md5">md5sum</a>)</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>USBシリアル変換は FTDI系を前提にしている。PL2303系（特にノーブランドのコピー品）は baud=115.2kではFIFOバッファが足りずに通信不具合が多発してトラブルのもとになる。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p><a href="https://www.centos.org">centos.org</a> <a href="http://multix.jp/headless-liveboot-centos/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p><a href="http://store.shopping.yahoo.co.jp/riava/rs670-s16a.html">Riava RS670A</a> 基本スペックは Intel Gigabit NICx6を持つ Intel/Atom C2758 でメモリ8GiB。基本OSは CentOSまたはUbuntu。SSD内蔵だがこれはここでは使用しない（壊さない）。なおフロントパネルのシリアルコンソールポート（COM0）はCisco互換配線（<a href="http://yost.com/computers/RJ45-serial">Yost Serial Device Wiring Standard</a>）になっている。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p><a href="https://fedorahosted.org/liveusb-creator/">Fedora LiveUSB Creator</a> <a href="http://multix.jp/headless-liveboot-centos/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p>persistentモードで初期化済フラグファイルが書けるようになると、これらの初期化処理はスキップされるようになる。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:5" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:6"><p>Fedora LiveUSB Creatorが、ISOイメージ中の /isolinux を /syslinux へコンバートしている。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:6" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:7"><p>Ubuntuでは USBメモリでも一応改竄チェックが可能だが、ISOイメージ中のブートセクタといった非ファイル領域に生じたライティングエラーを検出できない。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:7" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:8"><p>Dracutは、isomd5sumパッケージがインストール済なら initramfsを作成する際にこれを組み込む。initramfsに組み込まれていない場合、rd.live.checkオプションによるメディア検査は正しく実行できない。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:8" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:9"><p><em>Test this media &amp; start CentOS</em>は光学メディア起動時は chkisomd5が走り、検査に成功しなければログイン・プロンプトに達しないが、USBメモリ起動時はメディア自体が検査不能なので、そのまま進んでログイン・プロンプトに達することができる。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:9" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:10"><p>物理パーテションを /homeにしたり、暗号化したりする起動オプションもある。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:10" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[LinuxヘッドレスLiveBoot（Ubuntu編）]]></title><description><![CDATA[<p>VGAのない産業用ヘッドレスPC類に Linux系OSを組み込むのは割とよく行う。もっぱら CentOSをメインにしているのだが、時には諸事情やら比較検証用途やらで急遽 Ubuntuが必要になったりもする。そこでまあ Ubuntuベースのヘッドレス LiveBootを作っておくことにした。  </p>

<ul class="index"></ul>  

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する Ubuntuはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされている。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物はISOイメージとし、CD-R/DVD-Rに焼いてUSB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成のUSBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>apt-get一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>ubuntu</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的なLiveBootの流儀）  </li>
<li>既定のネットワーク設定・</li></ol>]]></description><link>http://multix.jp/headless-liveboot-ubuntu/</link><guid isPermaLink="false">2367878c-5496-47cb-962a-44089346dca7</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[Linux]]></category><category><![CDATA[めもらんだむ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Tue, 09 Jun 2015 02:27:30 GMT</pubDate><content:encoded><![CDATA[<p>VGAのない産業用ヘッドレスPC類に Linux系OSを組み込むのは割とよく行う。もっぱら CentOSをメインにしているのだが、時には諸事情やら比較検証用途やらで急遽 Ubuntuが必要になったりもする。そこでまあ Ubuntuベースのヘッドレス LiveBootを作っておくことにした。  </p>

<ul class="index"></ul>  

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する Ubuntuはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされている。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物はISOイメージとし、CD-R/DVD-Rに焼いてUSB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成のUSBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>apt-get一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>ubuntu</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的なLiveBootの流儀）  </li>
<li>既定のネットワーク設定・接続はオフとする。  </li>
<li>これらのパスワードやネットワーク設定は persistent有効時には自由に設定変更して永続化することができ、再起動しても設定内容が失われてはならない。</li>
</ol>

<p>要するに、KVMやDocker等の仮想マシン環境のそれと同様の感覚で、気軽に使用できる使い捨て環境の物理版<sup id="fnref:2"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:2" rel="footnote">2</a></sup>である。その仕様上基本的には RAMシステムとなり Swapは初期設定しない。従ってメインメモリは充分潤沢に搭載されているものとする。<sup id="fnref:3"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:3" rel="footnote">3</a></sup></p>

<p>Ubuntu LiveCD Customization については <strong><a href="https://help.ubuntu.com/community/LiveCDCustomization">こちらを参照</a></strong> のこと。</p>

<p>以後は ubuntu-15.04-desktop-amd64.iso をベースにして話を進める。32bit版でも同様だが、14.xx LTS では本稿で解説している手法は使えない。<sup id="fnref:4"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:4" rel="footnote">4</a></sup></p>

<p>なお製作時のリファレンス機材には Riava RS670A を使用した。<sup id="fnref:5"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:5" rel="footnote">5</a></sup></p>

<hr>

<h4 id="usb">シリアルコンソール対応のインストーラUSBフラッシュドライブを作る</h4>

<p>まずはヘッドレス環境で使用できるUbuntuインストーラを作らなければならない。UbuntuのインストーラISOには Desktop版と Server版があり、後者には CLI（テキスト）インストール機能があるが、本稿では Desktop版のISOイメージをベースにする。</p>

<p>端的には、内蔵ストレージにテキストベースの Ubuntuをクリーンインストールして GRUBで起動するのであれば、Server版をベースにするのが筋だ。だが Server版のISOイメージは LiveBoot機能を持たないため、今回の目的では Desktop版ISOイメージから作り出すほうが楽なのである。</p>

<p>用意するのは Ubuntu Desktop 64-bit Install ISO ファイルと、FAT32でフォーマットした1GiB以上のUSBフラッシュドライブと、Universal USB Installer<sup id="fnref:6"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:6" rel="footnote">6</a></sup>だ。</p>

<p>Windowsマシンで USBインストーラを立ち上げ、ダウンロードしたISOイメージと書き込み先フラッシュドライブを指定して起動可能メディアを作成する。この時 persistent領域は指定しない（永続化しない）。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-09-11-13-58.png" alt=""></p>

<p>次にブートメニューを書き換えてシリアルコンソールが使えるようにする。この作業は原則として TeraPadや MIFESのようなテキストエディタで行うが、Windows Notepadでもできなくはない。フラッシュドライブに書き込まれた元ファイルは LF改行なので Notepadでは全部が1行に繋がってしまうが、ブートローダー（SYSLINUX）は CR+LF改行でも動作するので、Notepadでも以下のテンプレートにそのまま入れ替えてしまってもよい。</p>

<p>isolinux.cfg冒頭のシリアルポート設定<sup id="fnref:7"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:7" rel="footnote">7</a></sup>は、BIOSにコンソールリダイレクト機能が備わっている場合には必要ない。BIOSとブートローダーの両方で設定をしてしまうと、入出力がバッティングして表示が崩れたりキー入力に不具合が生じる。従って今回の場合はコメントアウトで記述しておくだけとする。</p>

<pre><code class="language-brush:plain gutter:true highlight:[1,2] title:/isolinux/isolinux.cfg">#serial 0 115200
#console 0
path  
include menu.cfg  
default vesamenu.c32  
prompt 0  
timeout 50  
</code></pre>

<p>stdmenu.cfg からはスプラッシュ画像や表示色の設定をごっそり削る。またブートメニューが 80x24程度に収まるよう、vshift等を調整する。</p>

<pre><code class="language-brush:plain gutter:true title:/isolinux/stdmenu.cfg">menu vshift 4  
menu rows 10  
menu helpmsgrow 15  
menu cmdlinerow 16  
menu timeoutrow 16  
menu tabmsgrow 18  
menu tabmsg Press ENTER to boot or TAB to edit a menu entry  
</code></pre>

<p>txt.cfgのカーネルオプション行には、BIOSからカーネルに制御が渡った後のシリアルコンソール設定 <code>console=tty0 console=ttyS0,115200n8</code>を追記する。またLiveCD機能による自動ネットワーク構成が勝手に動かないよう <code>ip=frommedia</code>も必ず設定する。そしてこの段階では GUIと Ubuntuインストーラの起動をスキップしてシェルに落ちるための <mark>single</mark>を指定しておく。</p>

<pre><code class="language-brush:plain gutter:true highlight:[6] title:/isolinux/txt.cfg （USBメモリブート用）">menu title Ubuntu Console boot menu  
default live  
label live  
  menu label ^Try Ubuntu Console
  kernel /casper/vmlinuz.efi
  append  file=/cdrom/preseed/ubuntu.seed boot=casper cdrom-detect/try-usb=true noprompt floppy.allowed_drive_mask=0 ignore_uuid initrd=/casper/initrd.lz ip=frommedia console=tty0 console=ttyS0,115200n8 single ---
label memtest  
  menu label Test ^memory
  kernel /install/mt86plus
  append console=ttyS0,115200n8
label hd  
  menu label ^Boot from first hard disk
  localboot 0x80
</code></pre>

<p>追加された kernel起動パラメータは以下の意味を持つ。</p>

<ul>
<li><em>boot=casper</em> <br>
casperによるブートシーケンス（LiveBoot）を実行する。  </li>
<li><em>cdrom-detect/try-usb=true</em> <br>
USBメモリブートデバイスを CDROMと同等に扱う。  </li>
<li><em>floppy.allowed_drive_mask=0</em> <br>
フロッピーデバイスを無視する。</li>
<li><em>ignore_uuid</em> <br>
ブートメディアのUUIDをチェックしない。</li>
<li><em>ip=frommedia</em> <br>
ブートメディアの /etc/network/interfaces 設定を有効にする。</li>
<li><em>console=tty0</em> <br>
tty0をコンソール入出力に使用する。tty0は一般に VGA表示デバイスやフレームバッファである。  </li>
<li><em>console=ttyS0,115200n8</em> <br>
ttyS0をコンソール入出力に使用する。ttyS0は一般に第一シリアルポート。通信要件は baud=115200、パリティなし、8bit幅とする。このふたつのconsole指定は、両方書く場合はこの順番（tty0、ttyS0）で書かなければならない。<sup id="fnref:8"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:8" rel="footnote">8</a></sup>  </li>
<li><em>single</em> <br>
シングルモードで起動する。ここでは Ubuntuインストーラを起動させないために指定する。</li>
</ul>

<p>なおインストールイメージに含まれる memtest86+（mt86plus）は、シリアルコンソールに対応していない。通常のVGA表示に加えてBIOSコンソール表示もするようにリビルドしたバイナリと差し替えておくと便利だ。<sup id="fnref:9"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:9" rel="footnote">9</a></sup></p>

<hr>

<h4 id="rootfs">rootfsを作成する</h4>

<p>上記のUSBメモリで起動し <strong>Try Ubuntu Console</strong>を実行するとシングルユーザモードのbashシェルが起動する。まずは dhclientを起動したり、あるいは ifconfigと routeを叩くなりしてインターネットへ接続できるようにしよう。またロケール変数もセットする。</p>

<pre><code class="language-brush:bash">dhclient eth0

LANG=C  
</code></pre>

<p>現在起動しているこの環境は RAMシステムのため、8GiBの物理メモリを積んでいるなら 4GiB容量の RAMディスクが使える。これから作る rootfsは 2GiBも余裕があれば充分なので、/root以下を作業領域とする。なお /tmpは tmpfsマウント時のオプションのせいでそのままでは使用できない<sup id="fnref:10"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:10" rel="footnote">10</a></sup>。RAM容量が足りない場合には Ext4フォーマットした別の USBメモリや外付けHDD等を作業領域に使おう。</p>

<p>新しい rootfsは debootstrapパッケージを利用して作成する。さらにそれを LiveBootで使えるようにするために squashfs-toolsパッケージを用いるので、これらをインストールする。</p>

<pre><code class="language-brush:bash">apt-get install debootstrap squashfs-tools  
</code></pre>

<p>構築する Ubuntuのコードネームと CPUアーキテクチャは USBメモリから起動したそれと同じものとするが、これらは以下のコマンドで確認できる。</p>

<pre><code class="language-brush:plain highlight:[2,3,4,5,6,9]">lsb_release -a  
No LSB modules are available.  
Distributor ID: Ubuntu  
Description:    Ubuntu 15.04  
Release:        15.04  
Codename:       vivid

dpkg --print-architecture  
amd64  
</code></pre>

<p><em>lsb_release</em>コマンドが参照している実体は<code>/etc/lsb-release</code>なので、これをsourceコマンドで読めば環境変数で参照できるようになる。</p>

<pre><code class="language-brush:bash highlight:[2,3,4,5,10]">cat /etc/lsb-release  
DISTRIB_ID=Ubuntu  
DISTRIB_RELEASE=15.04  
DISTRIB_CODENAME=vivid  
DISTRIB_DESCRIPTION="Ubuntu 15.04"

source /etc/lsb-release

echo $DISTRIB_CODENAME  
vivid  
</code></pre>

<p>これらを debootstrapコマンドの引数に記述してミニマムな新規 rootfsを構築する。</p>

<pre><code class="language-brush:bash">source /etc/lsb-release  
mkdir newroot  
debootstrap --arch $(dpkg --print-architecture) \  
   $DISTRIB_CODENAME newroot http://archive.ubuntu.com/ubuntu
</code></pre>

<hr>

<h4 id="rootfs">rootfsの環境設定</h4>

<p>構築された rootfsは上記のコマンドだけで、すでに chrootして一応 apt-getも実行できる状態になっているが、kernelはまだインストールされていない。そこでこれらを行う前の準備として /proc、/dev、/run を rootfs内にマウントする。またデフォルトロケールとタイムゾーンをセットする。</p>

<pre><code class="language-brush:bash">mount -o bind /proc/ newroot/proc/  
mount -o bind /dev/ newroot/dev/  
mount -o bind /run/ newroot/run/

chroot newroot/ /usr/sbin/locale-gen en_US.UTF-8  
chroot newroot/ /usr/sbin/update-locale LANG=en_US.UTF-8  
chroot newroot/ /usr/sbin/dpkg-reconfigure locales  
chroot newroot/ /usr/bin/timedatectl set-timezone Asia/Tokyo  
</code></pre>

<p>さらに aptが参照する <code>newroot/etc/apt/sources.list</code>には必要最小限の1行しか書かれていないので、これを以下の内容に書き換える。<sup id="fnref:11"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:11" rel="footnote">11</a></sup></p>

<pre><code class="language-brush:plain title:newroot/etc/apt/sources.list for 15.04">deb http://jp.archive.ubuntu.com/ubuntu/ vivid main restricted  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid main restricted  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-updates main restricted  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-updates main restricted  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid universe  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid universe  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-updates universe  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-updates universe  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid multiverse  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid multiverse  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-updates multiverse  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-updates multiverse  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-backports main restricted universe multiverse  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-backports main restricted universe multiverse  
deb http://security.ubuntu.com/ubuntu vivid-security main restricted  
deb-src http://security.ubuntu.com/ubuntu vivid-security main restricted  
deb http://security.ubuntu.com/ubuntu vivid-security universe  
deb-src http://security.ubuntu.com/ubuntu vivid-security universe  
deb http://security.ubuntu.com/ubuntu vivid-security multiverse  
deb-src http://security.ubuntu.com/ubuntu vivid-security multiverse  
</code></pre>

<p>次に LiveBoot専用の管理者ユーザ <em>ubuntu</em>およびグループを作成し、パスワードなしでログイン出来るようにする。さらにrootアカウントでは直接ログイン出来ないようにロックしておく。このユーザ名は LiveBoot機能を提供している casperモジュールの設定に合わせたものだ。むろんそれぞれに任意のパスワードを設定しても構わない。（それを忘れたりしなければ）</p>

<pre><code class="language-brush:bash">chroot newroot/ /usr/sbin/addgroup --system --gid=999 ubuntu  
chroot newroot/ /usr/sbin/useradd -s /bin/bash -g ubuntu --uid=999 -m -k /dev/null ubuntu  
chroot newroot/ /usr/bin/passwd -d ubuntu  
chroot newroot/ /usr/sbin/usermod -L root  
</code></pre>

<p>内蔵ストレージに直接起動可能な rootfsをインストールする場合は、この時点で<code>/etc/fstab</code>を作成しなければならないが、LiveBootではこれが起動中に動的に作成・設定されるため、必要ない。</p>

<p>以上の環境設定が済んだら、apt-getで update/upgradeを行って chroot環境を更新する。</p>

<pre><code class="language-brush:bash">chroot newroot/ /usr/bin/apt-get update  
chroot newroot/ /usr/bin/apt-get upgrade  
</code></pre>

<hr>

<h4 id="linuxkernel">Linux kernelモジュールの組み込み</h4>

<p>いよいよ rootfs内に kernelモジュールをインストールするのだが、その前に現在起動中の USBメモリ（この例では/dev/sda）の MBRセクタを一応バックアップしておく。kernelインストール中に grub-pcセットアップが走るのだが、このとき MBRが上書きされてリブート不能<sup id="fnref:12"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:12" rel="footnote">12</a></sup>になるのを避けるためだ。なおインストールする kernelパッケージ名は 64bitと 32bitでは異なる。</p>

<pre><code class="language-brush:bash">dd if=/dev/sda of=mbr count=1  
</code></pre>

<pre><code class="language-brush:bash title:64bit kernel for EFI Support">chroot newroot/ /usr/bin/apt-get install linux-signed-image-$(uname -r)  
</code></pre>

<pre><code class="language-brush:bash title:32bit kernel">chroot newroot/ /usr/bin/apt-get install linux-image-$(uname -r)  
</code></pre>

<p>grub-pcセットアップは <em>Continue without installing GRUB?</em>を選んで何もせずに抜ける。そして忘れないうちにバックアップしておいた MBRを書き戻す。</p>

<pre><code class="language-brush:bash">dd if=mbr of=/dev/sda  
</code></pre>

<p>続いてその他のパッケージを追加インストールし、整合性を確認したのち、パッケージキャッシュを削除する。少なくとも <code>plymouth</code>はインストールが必要である。</p>

<pre><code class="language-brush:bash">chroot newroot/ /usr/bin/apt-get install plymouth squashfs-tools openssh-client  
chroot newroot/ /usr/bin/apt-get -f install  
chroot newroot/ /usr/bin/apt-get upgrade  
chroot newroot/ /usr/bin/apt-get autoremove  
chroot newroot/ /usr/bin/apt-get clean  
</code></pre>

<p>最後に newroot/var以下のクリーニング<sup id="fnref:13"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:13" rel="footnote">13</a></sup>と、newroot/bootから <strong>kernelとinitrdの削除</strong>する。LiveBootではブートデバイスの <code>casper</code>ディレクトリにある kernelとinitrdをそのまま使用するのでこれらの容量を節約する。</p>

<pre><code class="language-brush:bash">find newroot/var/log -type f -exec truncate -s 0 {} \;  
find newroot/var/cache -name \*-old -delete  
rm -fr newroot/var/lib/apt/lists/*

rm -f newroot/boot/vmlinuz-* newroot/boot/initrd.img-*  
</code></pre>

<p>以上で rootfsの構築は完了したのだが、作業時にマウントした /proc、/dev、/run がアンマウントできない場合がある。その際は lsofコマンドで掴んでいるプロセス探して killすればよい。正しくアンマウントできていれば duコマンドで使用容量が把握できる。</p>

<pre><code class="language-brush:bash highlight:[10]">lsof | grep newroot

kill &lt;PID&gt;

unmount newroot/run  
unmount newroot/dev  
unmount newroot/proc

du -sh newroot/  
420M    newroot/  
</code></pre>

<hr>

<h4 id="filesystemsquashfs">filesystem.squashfs ファイルの作成</h4>

<p>構築した rootfsを mksquashfsコマンドで圧縮する。このファイルはUSBメモリの /casperディレクトリに格納するが、これは <code>/cdrom/casper</code>として見ることが出来る。ただし <code>/cdrom</code>（USBメモリ）は roマウントされていてそのままでは書き込めないため、rwオプションでリマウントする。またそこにある <code>filesystem.squashfs</code>はいままさに <code>/rofs</code>にマウントして使われているため、直接上書きしてはならない。そこで別のファイル名で保存しておく。またこの rootfs全体のブロックサイズも保存しておく。</p>

<pre><code class="language-brush:bash">mount -o rw,remount /cdrom  
mksquashfs newroot /cdrom/casper/filesystem.squashfs.newroot  
printf $(du -s --block-size=1 newroot | cut -f1) &gt; /cdrom/casper/filesystem.size.newroot  
</code></pre>

<p>ちなみに <code>mksquashfs</code>で作成したファイルは <code>unsquashfs</code>コマンドで展開することが出来る。</p>

<pre><code class="language-brush:bash highlight:[4,5]">unsquashfs -d expand-root /cdrom/casper/filesystem.squashfs

ls expand-root/  
bin   dev  home        lib    media  opt   root  sbin  sys  usr  vmlinuz  
boot  etc  initrd.img  lib64  mnt    proc  run   srv   tmp  var  
</code></pre>

<hr>

<h4 id="rootfs">新 rootfs での起動テスト</h4>

<p>これらの作業を終えたら poweroff（shutdown -h）して、USBメモリを Windowsマシンに戻して以下の修正を行う。</p>

<ul>
<li>既存の /casper/filesystem.squashfs を filesystem.squashfs.org にリネーム</li>
<li>/casper/filesystem.squashfs.newroot を filesystem.squashfs にリネーム</li>
<li>同様に filesystem.size も差し替える。</li>
<li>/isolinux/txt.cfg の append行からキーワード single を削除する。</li>
</ul>

<p>この新たな環境での LiveBoot起動に失敗した場合は、上記の逆の手順でインストーライメージのシングルモードに戻す。そして unsquashfsコマンドで rootfsを展開して修正することを繰り返す。</p>

<p>新たな LiveBootが正しく起動してログイン・プロンプトが現れたら、ユーザ名 <em>ubuntu</em>（パスワード無し）でコンソールに入れるはずだ。また <code>sudo -s</code>で rootシェルになれる。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-22-18-14-35.png" alt=""></p>

<hr>

<h4 id="iso">ISOイメージを作成する</h4>

<p>こうして作成した filesystem.squashfsと、ブートメニューファイルを差し替えた ISOイメージを作成する。</p>

<p>まずベースになる Desktop版ISOイメージをローカルに展開する。これはODD（光学ドライブ）から読みだしても良いし、フラッシュドライブにISOイメージをコピーしておいて loopbackマウントで読みだしても良い。コピーしたファイルは書き込み不可となっているので <code>chmod</code>コマンドでパーミッションを変更する。</p>

<pre><code class="language-brush:bash highlight:[6,13,14]">cd /tmp

mkdir loop

sudo mount -o loop ubuntu-15.04-desktop-amd64.iso loop/  
mount: /dev/loop2 is write-protected, mounting read-only

cp -r loop files  
sudo umount loop  
chmod -R +w ./files

ls files/  
autorun.inf  casper  EFI      isolinux    pics  preseed             ubuntu  
boot         dists   install  md5sum.txt  pool  README.diskdefines  wubi.exe  
</code></pre>

<p>現在起動している filesystem.squashfsは <code>/cdrom/casper/</code>以下に見えているのでこれをコピーする。</p>

<pre><code class="language-brush:bash">cat /cdrom/casper/filesystem.squashfs &gt; files/casper/filesystem.squashfs  
</code></pre>

<p>isolinux/isolinux.cfg<sup id="fnref:14"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:14" rel="footnote">14</a></sup>や stdmenu.cfgについては前に上げたのと同じなので割愛する。新しい ODDブート用の isolinux/txt.cg は以下のように記述する。</p>

<pre><code class="language-brush:plain gutter:true title:isolinux/txt.cfg （ODDブート用）">menu title Ubuntu Console boot menu  
default live  
label live  
  menu label ^Try Ubuntu Console
  kernel /casper/vmlinuz.efi
  append  file=/cdrom/preseed/ubuntu.seed boot=casper initrd=/casper/initrd.lz quiet ip=frommedia console=tty0 console=ttyS0,115200n8 ---
label check  
  menu label ^Check disc for defects
  kernel /casper/vmlinuz.efi
  append  boot=casper integrity-check initrd=/casper/initrd.lz quiet console=tty0 console=ttyS0,115200n8 ---
label memtest  
  menu label Test ^memory
  kernel /install/mt86plus
  append console=ttyS0,115200n8
label hd  
  menu label ^Boot from first hard disk
  localboot 0x80
</code></pre>

<p>md5sum.txtは2番目の起動選択 <strong>Check disc for defects</strong>を選んだ際に参照される改竄チェックファイルだ。単純にこのファイルをゼロバイトに切り詰めれば何もチェックされない。一方この機能を活かしたい場合、チェックファイルを作成した後に md5sumが変わってしまうファイル、すなわち md5sum.txt自身と mkisofsコマンドが作成・更新する boot.cat、isolinux.bin はチェックリストから除外しておかなければならない。</p>

<pre><code class="language-brush:bash">pushd ./files  
rm -f md5sum.txt isolinux/boot.cat  
find . -type f -print | xargs md5sum | grep -v isolinux.bin &gt; ../md5sum.txt  
mv ../md5sum.txt .  
popd  
</code></pre>

<p>ISOイメージを作成する mkisofsコマンドは、同名のパッケージでインストールできる。</p>

<pre><code class="language-brush:bash">sudo apt-get install mkisofs  
</code></pre>

<p>mkisofsコマンドの引数<sup id="fnref:15"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:15" rel="footnote">15</a></sup>のうち -b（ブートローダーファイル）と-c（カタログファイル）は構築対象ディレクトリを基準とした場合のパス指定になるので注意しよう。また出力する ISOファイルは、先に上げた USBインストーラを使用する場合はその制限によりファイル名中に <em>desktop</em>の文字列が入っていなければならない。ともかく指定するオプションが多いのでシェルスクリプトにしておいたほうがコードの再利用と修正はしやすい。</p>

<pre><code class="language-brush:bash highlight:[14,15]">find ./files -print | xargs touch -m {} ¥;  
LANG=C mkisofs -r -J -l \  
  -V UBUNTU64 \
  -cache-inodes \
  -b isolinux/isolinux.bin \
  -c isolinux/boot.cat \
  -no-emul-boot \
  -boot-load-size 4 \
  -boot-info-table \
  -o ubuntu-15.04-desktopless-amd64.iso \
  ./files

ls -la ubuntu-15.04-desktop*  
-rwxr--r-- 1 ubuntu ubuntu 1150844928 Jun 17 16:59 ubuntu-15.04-desktop-amd64.iso
-rw-rw-r-- 1 ubuntu ubuntu  238929920 Jun 17 17:25 ubuntu-15.04-desktopless-amd64.iso
</code></pre>

<p>出来上がった ISOイメージはフラッシュドライブや NASなどにコピーして保存しておこう。</p>

<hr>

<h4 id="cdrwdvdrwiso">CD-RW/DVD-RWに ISOイメージを焼く</h4>

<p>前項で作成したISOイメージを Linuxで CD-RW/DVD-RWメディアに焼くには以下のようにする。</p>

<p>まずライティングソフトとして wodimパッケージをインストールする。</p>

<pre><code class="language-brush:bash">sudo apt-get install wodim  
</code></pre>

<p>USB-ODDは、おそらく /dev/sr0 等に見えるだろう。wodimコマンドの <code>-scanbus</code>オプションは SCSIバス以外には機能しないので、dmesgでデバイスノードを確認する。</p>

<pre><code class="language-brush:bash highlight:[2,3,4,5]">sudo wodim dev=/dev/sr0 --devices  
wodim: Overview of accessible drives (1 found) :  
-------------------------------------------------------------------------
 0  dev='/dev/sr0'      rwrw-- : 'Slimtype' 'eTAU108   1'
-------------------------------------------------------------------------
</code></pre>

<p>CD-RW/DVD-RWメディアをブランク消去するには wodimコマンドの blankオプションを使う。</p>

<pre><code class="language-brush:bash">sudo wodim dev=/dev/sr0 blank=fast  
</code></pre>

<p>wodimコマンドにISOイメージファイルを与えると、それを指定のドライブに焼きこむ。</p>

<pre><code class="language-brush:bash">sudo wodim -sao -eject dev=/dev/sr0 ubuntu-15.04-desktopless-amd64.iso  
</code></pre>

<hr>

<h4 id="persistent">persistentモードを設定する</h4>

<p>前述の ISOイメージ/光学メディアで ODDブートが正常に行えたなら、次はその ISOイメージを USBインストーラでもって、今度は persistentモードを設定した USBメモリを作成しよう。</p>

<p>persistentモードが有効な USBメモリは、kernel起動オプションに <em>persistent</em>が加わり、USBメモリのルートに <code>casper-rw</code>というファイルが作成される。このファイルの実体は overlayfsを用いて <code>/</code>に重ねられた ext4フォーマットの loopbackマウントファイルだ。<code>/rofs</code>にマウントされた filesystem.squashfsとの変更差分は、この <code>casper-rw</code>に記録されるようになる。</p>

<pre><code class="language-brush:plain highlight:[2,3,4,5,8,9,10,11,12]">df -Th  
Filesystem     Type      Size  Used Avail Use% Mounted on  
udev           devtmpfs  3.9G     0  3.9G   0% /dev  
tmpfs          tmpfs     798M  8.6M  789M   2% /run  
/dev/sda       vfat      2.0G  1.5G  468M  76% /cdrom
/dev/loop0     squashfs  182M  182M     0 100% /rofs
/cow           overlay   976M   14M  895M   2% /
tmpfs          tmpfs     3.9G     0  3.9G   0% /dev/shm  
tmpfs          tmpfs     5.0M     0  5.0M   0% /run/lock  
tmpfs          tmpfs     3.9G     0  3.9G   0% /sys/fs/cgroup  
tmpfs          tmpfs     3.9G     0  3.9G   0% /tmp  
tmpfs          tmpfs     798M     0  798M   0% /run/user/999  
</code></pre>

<p>persistentモードが正しく効いているかどうかは、更新したログインパスワードやネットワーク設定が再起動しても正しく機能するか否かで容易にわかる。例えばrootアカウントでログイン可能にし、ubuntuアカウントを無効にして新たにadminユーザを作成し、rootとadminにパスワードを設定してみよう。</p>

<pre><code class="language-brush:plain highlight:[6,7,8,13,14,19,20,21]">sudo -s

usermod -U root

passwd root  
Enter new UNIX password:  
Retype new UNIX password:  
passwd: password updated successfully

usermod -L ubuntu

addgroup --system admin  
Adding group `admin' (GID 112) ...  
Done.

useradd -s /bin/bash -g admin -m -k /dev/null admin

passwd admin  
Enter new UNIX password:  
Retype new UNIX password:  
passwd: password updated successfully  
</code></pre>

<p>/etc/network/interfaces に記述したネットワーク設定も persistentモードでなら起動時に反映される。<sup id="fnref:16"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:16" rel="footnote">16</a></sup></p>

<pre><code class="language-brush:plain gutter:true title:/etc/network/interfaces sample">source-directory /etc/network/interfaces.d

auto lo  
iface lo inet loopback

auto eth0  
iface eth0 inet static  
    address 192.168.240.135
    network 192.168.240.0
    netmask 255.255.255.0
    broadcast 192.168.240.255
    gateway 192.168.240.1
    dns-nameservers 192.168.240.1 8.8.8.8 8.8.4.4

auto eth4  
iface eth4 inet dhcp  
</code></pre>

<p>persistentモードで記録された内容を無視してデフォルト状態で起動するには、ブートメニューの起動ラベル選択時に <code>persistent</code>ラベルを取り除けば良い。あるいは casper-rwファイルをリネームして再起動してしまう。<sup id="fnref:17"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:17" rel="footnote">17</a></sup></p>

<pre><code class="language-brush:bash">cd /cdrom  
mv casper-rw casper-rw.bak  
reboot  
</code></pre>

<p>新たな（まっさらの）1GiBの casper-rwファイルは Linux上では以下の手順で作成できる。また事のついでなので、Windows上からもこのファイルを容易に置き換えられるように、フォーマットしたばかりの casper-rwファイルを zipコマンドで圧縮バックアップしておくと、USBインストーラを使わなくても最初期化できるので、いざというとき何かと便利だ。<sup id="fnref:18"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:18" rel="footnote">18</a></sup></p>

<pre><code class="language-brush:bash highlight:[8]">cd /cdrom

rm -f casper-rw.bak

truncate -s $((2**30)) casper-rw

ls -l casper-rw  
-rwxr-xr-x 1 root root 1073741824 Jun 18 13:11 casper-rw

mkfs.ext4 -L casper-rw casper-rw


apt-get install zip

zip casper-rw.zip casper-rw  
</code></pre>

<p>なお FAT32での最大のファイルサイズは 4GiB-1バイトなので、truncateコマンドでの容量指定は<code>$((2**32-1))</code>となる。<sup id="fnref:19"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:19" rel="footnote">19</a></sup></p>

<hr>

<h4 id="">ダウンロード</h4>

<p><a href="https://secure.multix.jp/download/SerialLive/ubuntu-15.04-desktopless-amd64-20150703.iso">ubuntu-15.04-desktopless-amd64-20150703.iso</a> 174MiB (<a href="https://secure.multix.jp/download/SerialLive/ubuntu-15.04-desktopless-amd64-20150703.iso.md5">md5sum</a>)</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>USBシリアル変換は FTDI系を前提にしている。PL2303系（特にノーブランドのコピー品）は baud=115.2kでは通信異常が多発してトラブルのもとになる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>ヘッドレスにしてディスクレスでもある。PXEBOOTなら USBメモリすら使わない完全ディスクレスだが、ネットワーク等の周辺環境規模が大きくなるので気軽にお試し出来るものではない。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>メインメモリは最低でも2GiB、標準的には8GiBは乗っているものとする。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p><a href="http://www.ubuntu.com/download/desktop">Ubuntu.com</a> 14.xx以前はシリアルコンソール自体に対応していない。ローカルストレージインストールの場合は /etc/init/ttyS0.confを作成すれば一応使えるようになるが、LiveBootはこの設定を壊してしまうため、15.04とおなじ挙動をさせるにはかなりの修正が必要になるため、本稿では 14.xx対応については割愛した。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p><a href="http://store.shopping.yahoo.co.jp/riava/rs670-s16a.html">Riava RS670A</a> 基本スペックは Intel Gigabit NICx6を持つ Intel/Atom C2758 でメモリ8GiB。基本OSは CentOSまたは Ubuntu。SSD内蔵だがこれはここでは使用しない（壊さない）。なおフロントパネルのシリアルコンソールポート（COM0）は Cisco互換配線（<a href="http://yost.com/computers/RJ45-serial">Yost Serial Device Wiring Standard</a>）になっている。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:5" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:6"><p><a href="http://www.pendrivelinux.com/universal-usb-installer-easy-as-1-2-3/">Universal USB Installer</a> <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:6" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:7"><p><a href="http://www.tldp.org/HOWTO/Remote-Serial-Console-HOWTO/configure-boot-loader-syslinux.html">Remote Serial Console HOWTO</a> <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:7" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:8"><p>最初に書いたほうが主コンソール <em>/dev/console</em>になり、そのデバイスは常に存在することが期待される。物理的にVGAを持たない機器では tty0なしの ttyS0だけでも実は良いのだが、シリアルポート未接続であったりシリアルポートドライバに問題がある場合は起動不能になる事態もありうる。そこで plymothを使うことでフレームバッファを用意し、tty0が必ず存在する状態をとすることで不具合を回避している。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:8" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:9"><p>ただし memtest86+のフル機能がシリアルコンソールで使えるわけではない。configメニュー表示等に不具合があるため、設定変更が難しい。このためECC関係などのオプションはビルド時に修正しておかなければならない。シリアルコンソール対応memtest86+の作成については<a href="http://multix.jp/buildup-memtest-headless/">別項を参照</a>のこと。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:9" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:10"><p>規定値でnosuid,nodev,noexecが指定されている。これらをremountで外しても良いが、最初から制限のかかっていない /rootや /homeを使うほうが手間がない。ただし後述の persistentモードが有効な場合、これらはRAMディスクではないので、<em>mount -t tmpfs none /mnt</em> といった具合に明示的に RAMディスクをマウントして使ったほうが良い。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:10" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:11"><p>デフォルトの1行だけでも必要最小限のことはできるが、docker等いまどきのアプリケーションをフルに動かせるようにするには、これだけの行数が必要になる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:11" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:12"><p>MBRが壊れるのは、起動したUSBメモリ以外の内蔵ストレージがある場合でかつ /dev、/run等のマウントを忘れていた場合に発生する。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:12" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:13"><p>ログファイルは単純な削除でも良いが、wtmpやlastlog等も区別なく初期化するために、ここではtruncateでゼロバイトに切り詰めている。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:13" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:14"><p>ISOからコピーしたオリジナルのisolinux.cfgには最後に<strong>gfxboot</strong>にチェインする行があるが本稿では使わない（ISOLINUXをブートローダーとする）ためこれは必ず削除しなければ起動不能になる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:14" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:15"><p>gfxboot関係については、GRUB設定ごと割愛した。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:15" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:16"><p>ただし kernel起動時に <em>ip=frommedia</em> をつけておかないと、ネットワーク設定は綺麗サッパリ無視されてしまうので気をつけよう。知らないと確実にハマる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:16" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:17"><p>persistentモードで起動しているときは当然マウントされているので削除したりdd初期化したりするのは論外だが、FAT32フォーマットの USBメモリで起動している場合はマウントしたまま mvコマンドでリネームすることはできてしまう。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:17" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:18"><p>ISOイメージを生成する前に準備しておくとより効果的だろう。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:18" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:19"><p>ブランクファイルを用意するのに ddコマンドがよく使われるが、バイト単位でのファイルサイズ指定は不得手なので truncateコマンドを使うほうが楽だし、そもそも動作が早い。あえて ddコマンドのオプションで 4GiB-1を表現するなら <em>bs=$((2**16-1)) count=$((2**16+1))</em> となる。  <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:19" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[気付いたらrootメールが溢れてる]]></title><description><![CDATA[<p>そんなことになる前にやれるべきことはやっておこう。</p>

<ul class="index"></ul>  

<hr>

<h4 id="">犯人はだれ</h4>

<p>Linuxサーバをテキトーに作ってテキトーに運用してるとやらかしがちなのがrootメールの始末だろう。ターミナルログインしてrootにsuしたら毎回<em>You have mail</em>と言われるアレである。mailコマンドを打ってもよくわからんメールが大量に溜まっておりいちいち読むのも削除するのも面倒だからと放置しがちだが、気を抜くと何年かして忘れた頃に突如ディスクが溢れて、システム丸ごと制御不能ということも稀にあるからあなどれない。それはまあ自業自得なのだが、ミニマルな普通のRHEL/CentOS（Linux）ならrootメールの生成元は基本的に以下のふたつだけだ。</p>

<ul>
<li>crond</li>
<li>logwatch</li>
</ul>

<p>たったふたつしかないのだからサーバセットアップ時に忘れずに対処しておくに越したことはない。あとからnagiosやらchkrootkitやらを動かしだすと当然メール生成元が増えてゆくわけだしその前に、である。とりあえず手堅いのは<code>/etc/aliases</code>を編集してrootメールを外部のまともなメールアドレスに丸ごと転送してしまうことだ。</p>

<hr>

<h4 id="aliases">aliasesで転送する</h4>

<p><code>/etc/aliases</code>の末尾を見ると次のようになっている。</p>

<pre><code class="language-brush:plain title:/etc/aliases"># trap decode to catch security attacks
decode:         root

# Person who should get root's mail
#root:          marc
</code></pre>

<p>見れば分かる通り <code>root: USERID</code>を追記すれば、 root宛のメールは指定のローカルアカウントに転送され、</p>]]></description><link>http://multix.jp/rootmail-overflow/</link><guid isPermaLink="false">fbdfc202-3271-4b0a-9b0b-41a7aecc7b13</guid><category><![CDATA[Linux]]></category><category><![CDATA[めもらんだむ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 08 Jun 2015 09:17:00 GMT</pubDate><content:encoded><![CDATA[<p>そんなことになる前にやれるべきことはやっておこう。</p>

<ul class="index"></ul>  

<hr>

<h4 id="">犯人はだれ</h4>

<p>Linuxサーバをテキトーに作ってテキトーに運用してるとやらかしがちなのがrootメールの始末だろう。ターミナルログインしてrootにsuしたら毎回<em>You have mail</em>と言われるアレである。mailコマンドを打ってもよくわからんメールが大量に溜まっておりいちいち読むのも削除するのも面倒だからと放置しがちだが、気を抜くと何年かして忘れた頃に突如ディスクが溢れて、システム丸ごと制御不能ということも稀にあるからあなどれない。それはまあ自業自得なのだが、ミニマルな普通のRHEL/CentOS（Linux）ならrootメールの生成元は基本的に以下のふたつだけだ。</p>

<ul>
<li>crond</li>
<li>logwatch</li>
</ul>

<p>たったふたつしかないのだからサーバセットアップ時に忘れずに対処しておくに越したことはない。あとからnagiosやらchkrootkitやらを動かしだすと当然メール生成元が増えてゆくわけだしその前に、である。とりあえず手堅いのは<code>/etc/aliases</code>を編集してrootメールを外部のまともなメールアドレスに丸ごと転送してしまうことだ。</p>

<hr>

<h4 id="aliases">aliasesで転送する</h4>

<p><code>/etc/aliases</code>の末尾を見ると次のようになっている。</p>

<pre><code class="language-brush:plain title:/etc/aliases"># trap decode to catch security attacks
decode:         root

# Person who should get root's mail
#root:          marc
</code></pre>

<p>見れば分かる通り <code>root: USERID</code>を追記すれば、 root宛のメールは指定のローカルアカウントに転送され、 rootのスプールには残らなくなる。従って普段コンソールログインに使用するアカウントを指定してやれば、 rootにならなければメールが来ていることに気付かない、という事態は避けられる。</p>

<p><code>/etc/aliases</code>を編集したら忘れずに <code>newaliases</code>を実行してMTAを更新しよう。</p>

<p>当然、USERIDの代わりに外部メールアドレスを記述すれば、 rootメールはすべてそこに送られる・・・はずなのだが、システムによってはうまく動かない。その秘密が直前にある  <code>decode: root</code>だ。コメントにある通りセキュリティ上の理由で rootメールが外部に送信されないよう制限をかけている。故にこの制限をコメントアウトすれば意図通りあらゆる rootメールが外部メールアドレスへ全転送されるようになる。</p>

<pre><code class="language-brush:plain highlight:[2,5] title:ほんとうにそれでよいのか"># trap decode to catch security attacks
#decode:        root

# Person who should get root's mail
root:           log@example.com  
</code></pre>

<p>言うまでもなくこの設定で万一あれやこれやをされるとたいへん面白くない事態になる。セキュリティ設定は無闇に外すべきではないのが筋であるから、いったん普段使いのログインアカウントに転送し、そちらで外部メールアドレスへ再転送するように設定するのが正しい。</p>

<pre><code class="language-brush:plain highlight:[5,7] title:メール送信にログインアカウントを使う"># trap decode to catch security attacks
decode:         root

# Person who should get root's mail
root:           charlie

charlie:        log@outer.example.com  
</code></pre>

<p>ログイン管理者が複数いるなら、カンマで区切って転送先を列挙すればめいめいに転送される。これは簡易的なメーリングリストの作成方法と同じだ。</p>

<pre><code class="language-brush:plain title:カンマ区切りで複数ユーザ指定">root:           charlie, dennis  
</code></pre>

<p>当たり前だが自分自身に転送すればループメールになってしまう。外部転送しつつローカルにもメールを残したい場合は、バックスラッシュをつけたエイリアス名を記述する。</p>

<pre><code class="language-brush:plain title:エイリアスバックスラッシュを使う">X charlie:        charlie, log@outer.example.com

O charlie:        \charlie, log@outer.example.com  
</code></pre>

<hr>

<h4 id="aliasesmailfro">aliasesで MailFroｍを書き換える</h4>

<p>ところで、当然ながら外部メールアドレスへの転送を行う場合は、リジェクトやらドロップやらでエラーメールが戻ってくる<sup id="fnref:1"><a href="http://multix.jp/rootmail-overflow/#fn:1" rel="footnote">1</a></sup>ことになる。これをまた転送してしまえば当然無限ループになるので、回避するために転送メールの Fromをカスタマイズする手がある。各メール生成元で個別に MailFromを設定するのが正道だが、上記のエイリアスの仕組みを使えば該当アカウントを通過するすべてのメールのFromをもれなく上書きしてしまえる。</p>

<pre><code class="language-brush:plain title:aliases設定">charlie:          \charlie, "|/var/local/fw-charlie-to-log.sh"  
owner-fw-charlie: \charlie  
</code></pre>

<pre><code class="language-brush:bash gutter:true title:/var/local/fw-charlie-to-log.sh">#!/bin/sh -
/usr/lib/sendmail -f owner-fw-charlie@mydomain log@outer.example.com
</code></pre>

<p>これは、<em>charlie</em>に届いたメールは自分自身のローカルに保存されるとともに、指定スクリプト<sup id="fnref:2"><a href="http://multix.jp/rootmail-overflow/#fn:2" rel="footnote">2</a></sup>の標準入力に送る。スクリプトの方は Fromを <em>charlie</em>のエイリアス <em>owner-fw-charlie</em><sup id="fnref:3"><a href="http://multix.jp/rootmail-overflow/#fn:3" rel="footnote">3</a></sup>に書き換えつつ外部の <em>log@outer.example.com</em>に送信する。こうしておいていざリジェクトメールが帰ってくると、それは <em>owner-fw-charlie</em>宛に着信することになる。 <em>owner-fw-charlie</em>のエイリアス設定行では <em>charlie</em>へのローカル保存のみを指示しているので、再度の外部転送は発生せず、ループメールもここで止めつつリジェクトを捨てずに保存させることができる。</p>

<p>この話はクラウドVPS等の運用でグローバルIPを持っておりかつ SMTPポートを開けてメール受信もする正規のサーバでのことだが、送信元サーバ自身でエラーメールを受け取らないような運用でも、同じように MailFromを書き換える価値はある。とは言えその場合は From偽装とやっていることは同じなので正しく着発信できループにもならないよう充分に気を払う必要がある。</p>

<hr>

<h4 id="crond">crondのメール送信を止める</h4>

<p>ここまでは各種ステータス＆ジョブメールを捨てずに外部転送して受け取り＆閲覧する前提の話<sup id="fnref:4"><a href="http://multix.jp/rootmail-overflow/#fn:4" rel="footnote">4</a></sup>だったが、ここからはメール生成そのものを元から止めてしまう方法である。</p>

<p>cronの出すジョブメールを止めるには、まず第一に <strong>cron実行されるジョブがstdoutもstderrも吐かないこと</strong>が最重要だ。出力が何もなければ cronメールは生成されない。本来cronジョブのバックグラウンド終了結果を実行ユーザに返す仕組みなので、その中身は個々のジョブが責任を持っている・・・が、そうも言ってられない事態も時にはあるもので、そういう時は <code>/etc/sysconfig/crond</code> の<code>CRONDARGS</code>を次のように修正し、 <code>service crond restart</code>する。</p>

<pre><code class="language-brush:plain gutter:true highlight:[3] title:/etc/sysconfig/crond"># Settings for the CRON daemon.
# CRONDARGS= :  any extra command-line startup arguments for crond
CRONDARGS="-m off"  
</code></pre>

<p>もう少し一般的な（汎用的な）メールの止め方としては、 <code>/etc/crontab</code>に <code>MAILTO=""</code>を書く方法だろう。この方法は RHEL/CentOSに限らず Linux全般や *BSDにも通用する。そして編集後には crondを再起動（あるいは<code>kill -HUP</code>）しなければならないのだが、どういうわけか誰もが頻繁に<strong>プロセス再起動を忘れる</strong>のは定番のお約束である。<sup id="fnref:5"><a href="http://multix.jp/rootmail-overflow/#fn:5" rel="footnote">5</a></sup></p>

<pre><code class="language-brush:plain gutter:true highlight:[5] title:/etc/crontab">SHELL=/bin/bash  
PATH=/sbin:/bin:/usr/sbin:/usr/bin  
MAILTO=root  
MAILTO="log@outer.example.com"  
MAILTO=""  
HOME=/

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
</code></pre>

<p>ところがこの方法には落とし穴がある。crontabファイルは <code>/etc</code>の下にあるものばかりではない。</p>

<pre><code class="language-brush:plain gutter:true title:/var/spool/cron/root"># /usr/bin/crontab -l
MAILTO="root"  
#* * * * * /etc/pound/poundchk.sh 1&gt;/dev/null 2&gt;&amp;1
* * * * * /etc/rc.d/secure_cleaning.pl
*/3 * * * * /etc/rc.d/dns_listen_flush.sh
</code></pre>

<p><code>/var/spool/cron/</code> 以下には各ユーザ別の crontabファイルを作ることが出来る。 <code>/etc/crontab</code>に比べると6カラム目のユーザ指定がない<sup id="fnref:6"><a href="http://multix.jp/rootmail-overflow/#fn:6" rel="footnote">6</a></sup>ことを除けば実質同じものだ。当然MAILTO環境変数も同様に指定できるので、こちらでもメール送信が設定されていないか確認したほうがよい。</p>

<p>言うまでもなく先に上げた <code>/etc/sysconfig/crond</code>でメールを止める方法はメール送信が完全に止まるので、MAILTO環境変数を改めて指定してもジョブメールが出たりすることはない。</p>

<hr>

<h4 id="logwatch">logwatchのレポートメールを止める</h4>

<p>logwatchについては、もっと高級なシステム監視（ZABBIXやNagios）を採用しているなら不必要なことがほとんどなので、logwatchパッケージ自体を削除して最初からなかったことにしてしまうのが一番シンプルで確実だ。</p>

<pre><code class="language-brush:plain">sudo yum remove logwatch  
</code></pre>

<p>logwatchはperlやsendmail（postfix）くらいにしか依存していないはずなので、依存性により本パッケージ削除できないという事態は滅多にない。だが事情により既存パッケージを削除したくない・できない<sup id="fnref:7"><a href="http://multix.jp/rootmail-overflow/#fn:7" rel="footnote">7</a></sup>等という運営上の理由があるならば、以下の設定で動作を止めることができる。</p>

<pre><code class="language-brush:bash">echo "DailyReport=No" &gt;&gt; /usr/share/logwatch/default.conf/logwatch.conf  
</code></pre>

<p>これは<code>/etc/cron.daily/0logwatch</code>が次のように書かれているためだ。</p>

<pre><code class="language-brush:bash gutter:true title:/etc/cron.daily/0logwatch">#!/bin/bash

DailyReport=`grep -e "^[[:space:]]*DailyReport[[:space:]]*=[[:space:]]*" /usr/share/logwatch/default.conf/logwatch.conf | head -n1 | sed -e "s|^\s*DailyReport\s*=\s*||"`

if [ "$DailyReport" != "No" ] &amp;&amp; [ "$DailyReport" != "no" ]  
then  
    logwatch
fi  
</code></pre>

<p><code>/etc/cron.daily/0logwatch</code>を削除したり実行パーミッションを落とせば良くないか？という発想は甘い。<code>yum update</code>の際に欠損ファイルが復旧されたり、パーミッションが元通りに修復されてしまう事態がおこるため、こういうものは設定ファイルで動作を止めるのが正義である。</p>

<hr>

<h4 id="root">溢れたrootメールを消す・証拠を残す</h4>

<p>以上の予防処置を講じるまでもなく rootメールが溢れたあとでは、まあだいたいシステム（のディスク残量）が終わっているのでこうするしかない。</p>

<pre><code class="language-brush:plain title:ゼロバイト消去"># &gt; /var/spool/mail/root 
</code></pre>

<p>ゼロバイト消去をするか、<code>rm -f</code>で削除するかは好みではあるが、いずれにせよ早急にディスク容量を回復する必要がある状況のはずだ。悠長に問題ファイルのバックアップを保存して・・・などという余裕はないだろう。そもそもファイルコピーするだけのリソース余地なぞ残っていまい。</p>

<p>だが障害報告書に貼り付けるだけの最小限の状況証拠は残しておきたい場合はどうするか。かろうじてリモートターミナル<sup id="fnref:8"><a href="http://multix.jp/rootmail-overflow/#fn:8" rel="footnote">8</a></sup>が繋がっているなら、ターミナルアプリのログ機能を利用して記録を取ることができる。要するにスプールファイルを catでコンソールに流して保存したりスクリーンショットを撮影したりする。</p>

<p>もっとも真正直に catで全部流すと何時間掛かるか分かったものではないので、 head と tailで最初と最後の数メールぶん程度を記録すれば充分だろう。少なくとも何時からメールが溜まり始め、何時までシステムが動いていたかの問題期間を特定することは出来るはずだ。</p>

<hr>

<h4 id="postfix">Postfixのドロップメールを消す</h4>

<p>rootメールを外部送信するようにしていながら送信先がなかったり間違っていた場合、maildropやbounceディレクトリが溢れることになる。「それ」が溢れていることが自明<sup id="fnref:9"><a href="http://multix.jp/rootmail-overflow/#fn:9" rel="footnote">9</a></sup>であるならば、敢えてディレクトリの中を lsしようとしたり、rm -fしようとしたりしてはいけない。<sup id="fnref:10"><a href="http://multix.jp/rootmail-overflow/#fn:10" rel="footnote">10</a></sup></p>

<pre><code class="language-brush:plain"># rm -f /var/spool/postfix/maildrop/*
Too many arguments  
</code></pre>

<p>この場合は該当ディレクトリごと <code>rm -fr</code>して改めてディレクトリを作りなおすか、 <code>find|xargs</code>で消すか、今時の findなら <code>-delete</code>オプションが備わっているので、それを使って消す。</p>

<pre><code class="language-brush:plain title:丸ごと作り直す方法（手数が多くて面倒かつミスもしやすい）"># rm -fr /var/spool/postfix/maildrop
# mkdir /var/spool/postfix/maildrop
# chown postfix:postdrop /var/spool/postfix/maildrop
# chmod 730 /var/spool/postfix/maildrop
</code></pre>

<pre><code class="language-brush:plain title:割と何処でも通用する消し方"># find /var/spool/postfix/maildrop/ -type f -print | xargs rm -f
</code></pre>

<pre><code class="language-brush:plain title:スマートかつ高速だが対応していないfindもある"># find /var/spool/postfix/maildrop/ -type f -delete
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>rootで外部送信しない理由は、これが攻撃手段に使えるからだ。 <a href="http://multix.jp/rootmail-overflow/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>スクリプトには当然Sendmail/Postfixから実行できるオーナーとパーミッションが付いていること。 <a href="http://multix.jp/rootmail-overflow/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>ここで使うエイリアス名は、ローカルに実在するアカウント名であってはならないし、<em>/etc/aliases</em>で他用途に使っているエイリアス名であってもならない。 <a href="http://multix.jp/rootmail-overflow/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>万一悪意ある攻撃を受けたり乗っ取られて勝手にあれこれされたりした場合、各種メールやsyslogを外部転送してあると事後調査が捗る。 <a href="http://multix.jp/rootmail-overflow/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p>crondは起動/再起動直後の1分間は実際には動作しないので、テストジョブは少なくとも2〜3分先に仕掛ける必要がある。この3分というのが結構長いので、虫取りを何回か繰り返しているとたいがい再起動してたかどうかも忘れてしまう。（SEの頭の方に虫が湧く） <a href="http://multix.jp/rootmail-overflow/#fnref:5" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:6"><p>この文法の違いを忘れて cronが正しく動かなかった話はよくある。文脈によってはどちらの crontabを扱っているつもりなのか再確認しないと危ない。通常 crontabコマンドはユーザレベルの定期ジョブ用で、システムタスク用には使わないだろう。だがこれを用いる最大の利点は crondの再起動が不要なことである。 <a href="http://multix.jp/rootmail-overflow/#fnref:6" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:7"><p>設計仕様書に何故か含まれていて、要らないのに消すのは面倒という案件とか。 <a href="http://multix.jp/rootmail-overflow/#fnref:7" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:8"><p>sshdはとうに死んでいて手に負えないことが大多数だろうが、その状況でもシリアルコンソールには何の影響もなく使えるのが普通である。 <a href="http://multix.jp/rootmail-overflow/#fnref:8" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:9"><p>だいたい dfと duコマンドで何処がディスクを食い潰しているかを絞り込む過程で見つけることになる。 <a href="http://multix.jp/rootmail-overflow/#fnref:9" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:10"><p>どうしても lsしたい場合は <strong>-U</strong>（ソートしない）オプションを使わなければならない。これを忘れてswapフリーズに陥ると手に負えなくなる。そうなったプロセスは <strong>kill -KILL でも止められない</strong>のだ。 <a href="http://multix.jp/rootmail-overflow/#fnref:10" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[VMware ESXi をシリアルコンソールで動かす]]></title><description><![CDATA[<p>VMware ESXi はデフォルトではハイパーバイザがホストPCの物理VGAコンソール（GPU）を専有してしまうが、正直これはハードウェア資源が勿体無い。そこでハイパーバイザをヘッドレス化しようという話。  </p>

<ul class="index"></ul>  

<hr>

<h4 id="bootbankbootcfg">/bootbank/boot.cfg の修正</h4>

<p>通常の手順で既にインストール＆稼働済のESXiホストが相手の場合、シリアルコンソール化するには起動設定ファイルに修正を加えるだけで済む。ESXiのバージョンによって多少異なるが、5.x系は<code>kernelopt</code>の1行にシリアルコンソール設定を加えるだけで良い。</p>

<pre><code class="language-brush:plain gutter:true highlight:[3] title:/bootbank/boot.cfg">bootstate=0  
kernel=tboot.b00  
kernelopt=no-auto-partition text nofb com1_baud=115200 com1_Port=0x3f8 tty2Port=com1 gdbPort=none logPort=none  
modules=b.b00 --- useropts.gz --- k.b00 ---</code></pre>]]></description><link>http://multix.jp/vmware-esxi-over-serialconsole/</link><guid isPermaLink="false">45fd0e5a-313d-49ca-b7f7-e53f223392e5</guid><category><![CDATA[Linux]]></category><category><![CDATA[めもらんだむ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Tue, 19 May 2015 03:29:00 GMT</pubDate><content:encoded><![CDATA[<p>VMware ESXi はデフォルトではハイパーバイザがホストPCの物理VGAコンソール（GPU）を専有してしまうが、正直これはハードウェア資源が勿体無い。そこでハイパーバイザをヘッドレス化しようという話。  </p>

<ul class="index"></ul>  

<hr>

<h4 id="bootbankbootcfg">/bootbank/boot.cfg の修正</h4>

<p>通常の手順で既にインストール＆稼働済のESXiホストが相手の場合、シリアルコンソール化するには起動設定ファイルに修正を加えるだけで済む。ESXiのバージョンによって多少異なるが、5.x系は<code>kernelopt</code>の1行にシリアルコンソール設定を加えるだけで良い。</p>

<pre><code class="language-brush:plain gutter:true highlight:[3] title:/bootbank/boot.cfg">bootstate=0  
kernel=tboot.b00  
kernelopt=no-auto-partition text nofb com1_baud=115200 com1_Port=0x3f8 tty2Port=com1 gdbPort=none logPort=none  
modules=b.b00 --- useropts.gz --- k.b00 --- a.b00 --- net-ixgb.v00 --- ata-pata.v00 --- ata-pata.v01 --- ata-pata.v02 --- ata-pata.v03 --- ata-pata.v04 --- ata-pata.v05 --- ata-pata.v06 --- ata-pata.v07 --- block-cc.v00 --- ehci-ehc.v00 --- s.v00 --- ima-qla4.v00 --- ipmi-ipm.v00 --- ipmi-ipm.v01 --- ipmi-ipm.v02 --- misc-cni.v00 --- misc-dri.v00 --- net-be2n.v00 --- net-bnx2.v00 --- net-bnx2.v01 --- net-cnic.v00 --- net-e100.v00 --- net-e100.v01 --- net-enic.v00 --- net-forc.v00 --- net-igb.v00 --- net-nx-n.v00 --- net-r816.v00 --- net-r816.v01 --- net-s2io.v00 --- net-sky2.v00 --- net-tg3.v00 --- ohci-usb.v00 --- sata-ahc.v00 --- sata-ata.v00 --- sata-sat.v00 --- sata-sat.v01 --- sata-sat.v02 --- sata-sat.v03 --- scsi-aac.v00 --- scsi-adp.v00 --- scsi-aic.v00 --- scsi-bnx.v00 --- scsi-fni.v00 --- scsi-hps.v00 --- scsi-ips.v00 --- scsi-lpf.v00 --- scsi-meg.v00 --- scsi-meg.v01 --- scsi-meg.v02 --- scsi-mpt.v00 --- scsi-mpt.v01 --- scsi-mpt.v02 --- scsi-qla.v00 --- scsi-qla.v01 --- scsi-rst.v00 --- uhci-usb.v00 --- imgdb.tgz --- state.tgz  
build=5.0.0-3.48.1851670  
updated=3  
</code></pre>

<p>このファイルは起動中なら上記のパスにある。そうでなければ起動ディスク中のVFATフォーマットされたパーテションの一つにある。<sup id="fnref:1"><a href="http://multix.jp/vmware-esxi-over-serialconsole/#fn:1" rel="footnote">1</a></sup></p>

<hr>

<h4 id="">ヘッドレスインストール</h4>

<p>ヘッドレスで起動できることが確認できたから、これを応用すればヘッドレスインストールだってできる。インストールディスクはブートローダーにISOLINUXを使用しているので、これを丸ごとtftpサーバの<code>/tftpboot/images/esx</code>にコピーして、要所をPXELINUX用に修正すればよい。この時の<code>pxelinux.cfg</code>は以下のようになる。</p>

<pre><code class="language-brush:plain gutter:true title:/tftpboot/pxelinux.cfg/default">serial 0 115200  
console 0

DEFAULT menu.c32  
MENU TITLE ESXi-5.x.x-XXXXXX-full Boot Menu  
NOHALT 1  
PROMPT 0  
TIMEOUT 80  
LABEL install  
  KERNEL images/esx/mboot.c32
  APPEND -c images/esx/boot.cfg text nofb com1_baud=115200 com1_Port=0x3f8 tty2Port=com1 gdbPort=none logPort=none
  ipappend 2
  MENU LABEL ESXi-5.x.x-XXXXXX-full ^Installer

LABEL hddboot  
 LOCALBOOT 0x80
 MENU LABEL ^Boot from local disk
</code></pre>

<p>1,2行目がSYSLINUXの表示をシリアルポート（0=最初のシリアルポート）にもリダイレクトするための設定<sup id="fnref:2"><a href="http://multix.jp/vmware-esxi-over-serialconsole/#fn:2" rel="footnote">2</a></sup>だが、<strong>PCのBIOS自体にリダイレクトコンソールサポートがある場合は必要ない</strong>。これは存外見落としがちなのだが、BIOSでそれが有効な場合、SYSLINUX（やGRUB）のほうでも有効にしてしまうと入出力が二重化してしまい、特にキーボード入力で不具合が生じる。従ってこれはBIOSリダイレクトコンソールを使えない環境・設定においてのみ必須となる。</p>

<p>一方10行目のカーネルパラメータは、コンソール制御がBIOSやSYSLINUXから離れてカーネルに移ってから有効になるものなので、BIOSリダイレクトコンソールの有無に関わらず必要となる。</p>

<pre><code class="language-brush:plain gutter:true highlight:[5] title:/tftpboot/images/esx/boot.cfg (インストーラ起動用)">bootstate=0  
title=Loading ESXi installer  
prefix=images/esx/  
kernel=tboot.b00  
kernelopt=runweasel text nofb com1_baud=115200 com1_Port=0x3f8 tty2Port=com1 gdbPort=none logPort=none  
modules=b.b00 --- useropts.gz --- k.b00 --- chardevs.b00 --- a.b00 --- user.b00 --- s.v00 --- ata_pata.v00 --- ata_pata.v01 --- ata_pata.v02 --- ata_pata.v03 --- ata_pata.v04 --- ata_pata.v05 --- ata_pata.v06 --- ata_pata.v07 --- block_cc.v00 --- ehci_ehc.v00 --- weaselin.t00 --- esx_dvfi.v00 --- xlibs.v00 --- ima_qla4.v00 --- ipmi_ipm.v00 --- ipmi_ipm.v01 --- ipmi_ipm.v02 --- misc_cni.v00 --- misc_dri.v00 --- net_be2n.v00 --- net_bnx2.v00 --- net_bnx2.v01 --- net_cnic.v00 --- net_e100.v00 --- net_e100.v01 --- net_enic.v00 --- net_forc.v00 --- net_igb.v00 --- net_ixgb.v00 --- net_nx_n.v00 --- net_r816.v00 --- net_r816.v01 --- net_s2io.v00 --- net_sky2.v00 --- net_tg3.v00 --- net_vmxn.v00 --- ohci_usb.v00 --- sata_ahc.v00 --- sata_ata.v00 --- sata_sat.v00 --- sata_sat.v01 --- sata_sat.v02 --- sata_sat.v03 --- sata_sat.v04 --- scsi_aac.v00 --- scsi_adp.v00 --- scsi_aic.v00 --- scsi_bnx.v00 --- scsi_fni.v00 --- scsi_hps.v00 --- scsi_ips.v00 --- scsi_lpf.v00 --- scsi_meg.v00 --- scsi_meg.v01 --- scsi_meg.v02 --- scsi_mpt.v00 --- scsi_mpt.v01 --- scsi_mpt.v02 --- scsi_qla.v00 --- scsi_qla.v01 --- scsi_rst.v00 --- uhci_usb.v00 --- tools.t00 --- xorg.v00 --- imgdb.tgz --- imgpayld.tgz  
build=  
updated=0s  
</code></pre>

<p>インストーラ起動用の<code>boot.cfg</code>にもシリアルポート用の設定を加える。またネットワークブートカーネルのパスも修正が必要だ。</p>

<p>以上でESXiインストーラを動かせるはずだがインストール終了後に、最初に述べた<code>/bootbank/boot.cfg</code>の修正が必要だ。ここを自動化したい場合にはインストーライメージ内からkickstartファイルを見つけて、postスクリプトセクションに以下の1行を加えておく。</p>

<pre><code class="language- brush:plain">sed -i '/no-auto-partition/ s/$/ text nofb com1_baud=115200 com1_Port=0x3f8 tty2Port=com1 gdbPort=none logPort=none/' /bootbank/boot.cfg  
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>全く同じ内容の予備パーテションもあるが、通常はディスク先頭に近いほうがプライマリとして使われている。なお4行目のmodules読込設定は実ハード環境によって異なるので、このままコピペしないこと。 <a href="http://multix.jp/vmware-esxi-over-serialconsole/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p><a href="http://www.tldp.org/HOWTO/Remote-Serial-Console-HOWTO/configure-boot-loader-syslinux.html">Remote Serial Console HOWTO</a> <a href="http://multix.jp/vmware-esxi-over-serialconsole/#fnref:2" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[DynDNS互換サービスを自作する]]></title><description><![CDATA[<p>筆者が使っている Allied Telesisの AR560S/AR550SにはダイナミックDNS機能があるが、対応サービスにはStandard DNS(Dyn.com)しか使えない。まあ企業用途なら一口25ドルくらいどうってことはないが、個人用途でこれはちょっと面白くないので、DynDNS互換サービスを自作してみた話。</p>

<ul class="index"></ul>

<hr>

<h4 id="dyndns">DynDNSプロトコル</h4>

<p>まず基本的な情報だが、ARルータ側の設定項目はこう定義されている。</p>

<pre><code class="language-brush:plain">SET DDNS [SERVER=server] [PORT=port] [USER=userid] [PASSWORD=password] [DYNAMICHOST=hostnames] [PRIMARYINT=ipinterface] [SECONDARYINT=ipinterface] [OFFLINE={YES|NO|ON|OFF}] [PERIODICUPDATE={1..60|ON|OFF}]

server: ダイナミックDNSサーバー名。（1～31文字。英数字）  
port: HTTPポート番号。80(</code></pre>]]></description><link>http://multix.jp/create-compatible-dyndns-service/</link><guid isPermaLink="false">05d79948-33cb-4328-b681-cf62acedd527</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 15 May 2015 06:50:57 GMT</pubDate><content:encoded><![CDATA[<p>筆者が使っている Allied Telesisの AR560S/AR550SにはダイナミックDNS機能があるが、対応サービスにはStandard DNS(Dyn.com)しか使えない。まあ企業用途なら一口25ドルくらいどうってことはないが、個人用途でこれはちょっと面白くないので、DynDNS互換サービスを自作してみた話。</p>

<ul class="index"></ul>

<hr>

<h4 id="dyndns">DynDNSプロトコル</h4>

<p>まず基本的な情報だが、ARルータ側の設定項目はこう定義されている。</p>

<pre><code class="language-brush:plain">SET DDNS [SERVER=server] [PORT=port] [USER=userid] [PASSWORD=password] [DYNAMICHOST=hostnames] [PRIMARYINT=ipinterface] [SECONDARYINT=ipinterface] [OFFLINE={YES|NO|ON|OFF}] [PERIODICUPDATE={1..60|ON|OFF}]

server: ダイナミックDNSサーバー名。（1～31文字。英数字）  
port: HTTPポート番号。80(HTTP)または 8245(HTTP Bypass)  
userid: ユーザーID。（1～15文字。英数字。大文字小文字を区別する）  
password: パスワード（1～15文字。英数字。大文字小文字を区別する）  
hostnames: ホスト名（URL形式。カンマ区切りで複数指定可能。カンマを含め128文字まで。）  
ipinterface: インターフェース名。(ppp0など)  
</code></pre>

<p>その結果、DNSサーバには次のようなリクエストが送信される。</p>

<pre><code class="language-brush:plain">GET /nic/update?system=dyndns&amp;hostname=example.dyndns.org&amp;myip=123.45.67.89&amp;wildcard=OFF&amp;offline=OFF HTTP/1.1  
Host: members.dyndns.org  
Authorization: Basic [BASE64-ENCODED-USERNAME:PASSWORD-PAIR]  
User-Agent: Dyn DNS Client/CentreCOM AR560S version 2.9.2-09 21-Aug-2012  
</code></pre>

<p>これに対してサーバレスポンスに<code>good &lt;IP_ADDR&gt;</code>が返るとそれは正常に受け付けられたことになり、<code>show ddns</code>で確認できるステータス表示が更新される。</p>

<p>送信URIとして<code>/nic/update</code>は固定、ユーザ認証はBASIC認証方式、パラメータとして<code>hostname</code>は必須。CGIインタフェースとして実装すべき内容はこれだけなのでそう難しいことではない。</p>

<p>ここでの実装では要件的には必要ないのだがエラーログを残すのにも使うので、good以外のレスポンス文字列も上げておく。<sup id="fnref:1"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:1" rel="footnote">1</a></sup></p>

<pre><code class="language-table title:DynDNS Responce Code">|応答|状態|説明|
|good &lt;IP_ADDR&gt;|OK|リクエストは正常に受け付けた|
|nochg &lt;IP_ADDR&gt;|OK|リクエストは正常だが更新されるステータスはない|
|nohost|NG|hostnameは存在しない|
|badauth|NG|認証に失敗した|
|badagent|NG|この機器は許可されていない|
|!donator|NG|利用料が支払われていない|
|abuse|NG|ユーザはブロックされている・乱用|
|911|NG|重大なエラー|
</code></pre>

<hr>

<h4 id="nginxperlmodule">nginx perl moduleでの実装</h4>

<p>今回はnginxのperl module機能を用いて実装する。CentOS版のnginxはこれに対応しているが、公式サイトで配布しているバイナリは対応していないため要リビルドである。次のようにしてなにも表示されなければ、このビルドオプションが足りていない。</p>

<pre><code class="language-brush:plain">$ nginx -V 2&gt;&amp;1 | tr ' ' '\n' | grep perl
--with-http_perl_module
</code></pre>

<p>perlモジュールが有効なら、レスポンスハンドラをperlで記述することが出来る。そこで以下のコードを<code>/etc/nginx/lib/dyndns.pm</code>に用意した。</p>

<pre><code class="language-brush:perl gutter:true title:/etc/nginx/lib/dyndns.pm">#
# $Id: dyndns.pm 159 2015-05-15 04:18:23Z askn $
#
package dyndns;  
use strict;  
use utf8;  
use 5.010;  
use nginx;  
use Net::DNS;  
use MIME::Base64;  
use Sys::Syslog;

sub update_handler {  
    my $r = shift;

    $r-&gt;send_http_header("text/plain");
    return OK if $r-&gt;header_only;

    eval {
        openlog("ddns_update", "cons,pid", "daemon");

        die "unknown:nothing parameters\n" unless $r-&gt;args;

        my $auth = $r-&gt;header_in("Authorization") // "";

        die "badauth:bad authorization.\n"
            unless $auth =~ m{^Basic ([0-9a-z\+\/\=]+)}i;

        my $user = (split /:/, decode_base64($1))[0];

        my $q = {};
        foreach my $pair (split /&amp;/, $r-&gt;args) {
            my($key, $val) = split /=/, $pair;
            next      unless defined $key;
            $val = "" unless defined $val;
            $q-&gt;{$r-&gt;unescape($key)} = $r-&gt;unescape($val);
        }

        my $hostname = lc($q-&gt;{hostname} // "");
        my $myip     =    $q-&gt;{myip} || $r-&gt;remote_addr;
#        my $system   =    $q-&gt;{system}   // "";
#        my $wildcard =    $q-&gt;{wildcard} // "";
#        my $offline  =    $q-&gt;{offline}  // "";
#        my $mx       = ls($q-&gt;{mx}       // "");

        syslog("info", "USER=".$user." HOSTNAME=".$hostname." MYIP=".$myip);

        die "unknown:badformat myip.\n"
            unless $myip =~ /^(\d{1,3}\.){3}\d{1,3}$/;

        my $resolver = new Net::DNS::Resolver;
        $resolver-&gt;nameservers("127.0.0.1");

        my @fqdn = split /,/, $hostname;

        die "numhosth:too many hostname.\n"
            if 4 &lt; scalar @fqdn;

        foreach my $fqdn (@fqdn) {

        # ZONE name resolv stage

            defined $fqdn and $fqdn =~ s{(^\s+|\s+$)}{}g;

            die "nohost:badformat hostname.\n"
                unless defined $fqdn;

            die "badfqdn:badformat hostname.\n"
                unless $fqdn =~ /^([\w\-]+\.)*[\w\-]+$/;

            my $zone = "";
            my $ns = "";
            my(@stack, @ns);
            foreach my $dotted (reverse(split /\./, $fqdn)) {
                $zone = $dotted.".".$zone;
                unshift @stack, $zone;
            }

            foreach my $test (@stack) {
                my $query = $resolver-&gt;query($test, "NS");
                if ($query) {
                    foreach my $rr (grep {$_-&gt;type eq "NS"} $query-&gt;answer) {
                        push @ns, $rr-&gt;nsdname;
                    }
                    $zone = $test;
                    syslog("info", "ZONE=".$zone." NS=".join(",", @ns));
                    last;
                }
            }
            die "dnserr:Undefined query nameservers.\n"
                unless @ns;

            $resolver-&gt;nameservers(@ns);

        # Update stage

            my $update = new Net::DNS::Update($zone);
            $update-&gt;push(update =&gt; rr_del($fqdn.". IN A"));
            $update-&gt;push(update =&gt; rr_add($fqdn.". 3600 IN A ".$myip));

            my $reply = $resolver-&gt;send($update);

            die "dnserr:reply resolver ".$resolver-&gt;errorstring."\n"
                unless $reply;

            die "dnserr:reply ".$reply-&gt;header-&gt;rcode."\n"
                unless $reply-&gt;header-&gt;rcode eq "NOERROR";
        }

        $r-&gt;print("good " . $myip . "\n");
        syslog("info", "update success.");
    };
    if ($@) {
        chomp $@;
        syslog("info", "ERROR: " . $@);

        my $result = (split /:/, $@)[0];
        $r-&gt;print($result."\n");
    }

    return OK;
}
1;  
__END__  
</code></pre>

<p>コード中の<code>$r</code>はnginxから渡されるリファレンスオブジェクトで、mod_perlのそれと似た表現になっている。<code>$r-&gt;args</code>に送信されたクエリが入っているのでこれをパースして<code>hostname</code>と<code>myip</code>を受け取っているが、<code>myip</code>がなければ<code>$r-&gt;remote_addr</code>を代わりに使うよう<sup id="fnref:2"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:2" rel="footnote">2</a></sup>にもしている。その他のパラメータについてはこのコードでは対応していない。なお処理過程のログに付いては（perlの）<code>Sys::Syslog</code>モジュールを用いてsyslogのdaemonファシリティに送信するようにしている。</p>

<p>DNS Aレコード処理は（perlの）<code>Net::DNS</code>モジュールで行う。まず<code>localhost</code>ネームサーバにFQDNを問い合わせてNSレコード=当該ネームサーバIPを取得し（resolvステージ）そこに対して既存DNS Aレコードの削除および新規DNS Aレコードの登録（updateステージ）を行う。ここでは処理を端折っているが、本格的な汎用サービス目的で実装するなら、updateステージではアクセス認証キーの処理を追加する必要があるだろう。<sup id="fnref:3"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:3" rel="footnote">3</a></sup></p>

<hr>

<h4 id="nginxconf">nginx.conf側の記述</h4>

<p>nginx.confから要点だけを抜き書きすると次のようになる。</p>

<pre><code class="language-brush:plain">http {  
    perl_modules  /etc/nginx/lib;
    perl_require  dyndns.pm;

    server {
        location /nic/update {
            auth_basic            "closed site";
            auth_basic_user_file  /etc/nginx/_htpasswd;
            perl                  dyndns::update_handler;
        }
    }
}
</code></pre>

<p>まずグローバル設定内で<code>perl_modules</code>と<code>perl_require</code>ディレクティブを用いてperlコードをnginx本体にロードする。プロセス起動時にメモリ上に読み込まれるためコードを書き換えた場合は逐一nginxをreloadする必要がある。ここで読み込まれたperlコードのpackage名前空間名とハンドラ関数名をlocaltionブロック内で指定すると、URL（<code>/nic/update</code>）とperlコードとを紐付けることができる。またこの時BASIC認証を行うので、認証用パスワードファイルを<code>auth_basic_user_file</code>ディレクティブで指定する。</p>

<hr>

<h4 id="namedconf">named.conf側の設定</h4>

<p>named.confの設定では、FQDNが属すZONEを有しており、かつ上記のリクエストレコード送信元IPがallow-updateディレクティブで指定されていればよい。</p>

<pre><code class="language-brush:plain">zone "example.org" IN {  
    type master;
    :
    allow-update {
        127.0.0.1;
        192.168.0.0/24;
        key host1;
    }
}
</code></pre>

<hr>

<h4 id="perlcgi">参考：Perl-CGI版</h4>

<p>nginx版の前に作成・使用していたPerl-CGIバージョンを以下に上げておく。こちらは単に<code>nsupdate</code>コマンドを叩くだけのシンプルなものだ。<sup id="fnref:4"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:4" rel="footnote">4</a></sup></p>

<pre><code class="language-brush:perl collapse:true gutter:true title:/var/www/html/nic/update">#!/usr/bin/perl
use strict;  
use CGI;  
use Sys::Syslog;  
use 5.010;

my($q, $hostname);  
my $myip = '0.0.0.0';  
eval {  
    openlog('ddns_update', 'cons,pid', 'daemon');

    $q = new CGI;
    $hostname = $q-&gt;param('hostname');
    $myip     = $q-&gt;param('myip') || $ENV{HTTP_X_FORWARDED_FOR} || $ENV{REMOTE_ADDR};
#    $system   = $q-&gt;param('system');
#    $wildcard = $q-&gt;param('wildcard');
#    $offline  = $q-&gt;param('offline');
#    $mx       = $q-&gt;param('mx');

    syslog('info', 'HOSTNAME='.$hostname.' MYIP='.$myip);

    unless ($hostname =~ /^([\w\-]+\.)*[\w\-]+$/) {
        die "nohost\n";
    }

    unless ($myip =~ /^(\d{1,3}\.){3}\d{1,3}$/) {
        die "unknown\n";
    }

    open my $PIPE, '|-', '/usr/bin/nsupdate' or die "badcall nsupdate.";
    print $PIPE &lt;&lt;__EOF;
server 127.0.0.1  
update delete ${hostname}. IN A  
update add ${hostname}. 3600 IN A ${myip}  
send

__EOF  
    close $PIPE or die "break nsupdate.\n";

    print 'Content-type: text/plain', "\r\n\r\n";
    print 'good ', $myip, "\n";
    syslog('info', 'Success.');
};
if ($@) {  
    print 'Content-type: text/plain', "\r\n\r\n";
    print 'nochg ', $myip, "\n";
    syslog('info', 'ERROR: '.$@);
}
1;  
__END__  
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>911は米国で言う110番のこと。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>通常の用途ではMYIPとremote_addrは常に同一だろう。ルータを多段に組んだ場合でもなければ両者が異なるケースはまずない。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>ここでの実装は簡易的なので、ネームサーバ側の設定でこのサービスサーバのIPに直接update許可を与えている。キー認証を使用する場合、named.confのほうではallow-updateディレクティブにアクセス許可するキーIDを追記する。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>この実装のままnginx/perlに持って行くとfork負荷が問題になったのでNet::DNS実装に置き換えた。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:4" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[日本国内限定IPv4アクセス制限の仕込み方]]></title><description><![CDATA[<p>Webサービスとかやってると世界中と繋がっているのだなと思うことは多々ある。だがサービス内容によってはその範囲を狭めたいこともあるし、それ以前にお断りということもある。ではどうやって日本国内からのIPアクセスとそれ以外を区別しよう？</p>

<ul class="index"></ul>

<hr>

<h4 id="ipapnic">IPアドレスリストをAPNICから入手する</h4>

<p>世界中に分配されているIPv4アドレスは総元締めのIANAから、世界を分割支配する5つの団体（ARIN/APNIC/LACNIC/AfriNIC/RIPE NCC）に分配され、そこからさらに各国別の元締め（日本ならJPNIC）に再分配されている。このIP分配リストは各団体のFTPサイト等で公開されているので、これを入手して日本にアサインされたIPリストだけを抽出すればACL（アクセス制限リスト）の元を作ることが出来る。今回の場合はJPNICに割り当てられたIPリストを得るために、APNICのFTPサイトから更新リストを入手する。</p>

<p>この更新リスト<sup id="fnref:1"><a href="http://multix.jp/delegated-apnic-acl/#fn:1" rel="footnote">1</a></sup>はほぼ毎日アップデートされている。ファイルサイズは1.8MBほどで約4万行弱ある。また世界中からダウンロードに来ることもあって、日に何回もアクセスにゆくと回数制限に引っ掛かって数時間ブロックされるということもある。故に基本的には1日1回、cronを使用してダウンロードすることになる。</p>

<p>一方で毎日更新されているとはいえ、日本にアサインされたIPが日々こまめに変化しているというわけでもない。だいたい週に1〜2回程度、僅かな追加と削除<sup id="fnref:2"><a href="http://multix.jp/delegated-apnic-acl/#fn:2" rel="footnote">2</a></sup>が発生している程度だ。そこでダウンロードと共に前回のリストと比較して変化があったらメール通知を行い、さらにACLの更新処理を行うという仕組みを作ることにする。</p>

<p>なお以下の仕掛けはすべてrootユーザが行い、メインのワークエリアとしては<code>/etc/rc.d</code>を使用するものとする。</p>

<pre><code class="language-brush:bash gutter:true title:/etc/cron.daily/apnic.list_update">#!/bin/sh

### /etc/</code></pre>]]></description><link>http://multix.jp/delegated-apnic-acl/</link><guid isPermaLink="false">0bd8198c-10c5-4c3d-af4d-0aed7c19e87c</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Web]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Wed, 13 May 2015 09:08:40 GMT</pubDate><content:encoded><![CDATA[<p>Webサービスとかやってると世界中と繋がっているのだなと思うことは多々ある。だがサービス内容によってはその範囲を狭めたいこともあるし、それ以前にお断りということもある。ではどうやって日本国内からのIPアクセスとそれ以外を区別しよう？</p>

<ul class="index"></ul>

<hr>

<h4 id="ipapnic">IPアドレスリストをAPNICから入手する</h4>

<p>世界中に分配されているIPv4アドレスは総元締めのIANAから、世界を分割支配する5つの団体（ARIN/APNIC/LACNIC/AfriNIC/RIPE NCC）に分配され、そこからさらに各国別の元締め（日本ならJPNIC）に再分配されている。このIP分配リストは各団体のFTPサイト等で公開されているので、これを入手して日本にアサインされたIPリストだけを抽出すればACL（アクセス制限リスト）の元を作ることが出来る。今回の場合はJPNICに割り当てられたIPリストを得るために、APNICのFTPサイトから更新リストを入手する。</p>

<p>この更新リスト<sup id="fnref:1"><a href="http://multix.jp/delegated-apnic-acl/#fn:1" rel="footnote">1</a></sup>はほぼ毎日アップデートされている。ファイルサイズは1.8MBほどで約4万行弱ある。また世界中からダウンロードに来ることもあって、日に何回もアクセスにゆくと回数制限に引っ掛かって数時間ブロックされるということもある。故に基本的には1日1回、cronを使用してダウンロードすることになる。</p>

<p>一方で毎日更新されているとはいえ、日本にアサインされたIPが日々こまめに変化しているというわけでもない。だいたい週に1〜2回程度、僅かな追加と削除<sup id="fnref:2"><a href="http://multix.jp/delegated-apnic-acl/#fn:2" rel="footnote">2</a></sup>が発生している程度だ。そこでダウンロードと共に前回のリストと比較して変化があったらメール通知を行い、さらにACLの更新処理を行うという仕組みを作ることにする。</p>

<p>なお以下の仕掛けはすべてrootユーザが行い、メインのワークエリアとしては<code>/etc/rc.d</code>を使用するものとする。</p>

<pre><code class="language-brush:bash gutter:true title:/etc/cron.daily/apnic.list_update">#!/bin/sh

### /etc/rc.d/apnic.list_update
###
### delegated-apnic-latest を取得して変化があれば
### メール通知およびrc.iptablesの再実行を行うスクリプト
###
### /etc/cron.daily からsymlinkを張るか、そこに配置すること
###

new=`mktemp`  
errors=`mktemp`  
DIR="/etc/rc.d"  
list="$DIR/delegated-apnic-latest"

test -f $list || touch $list  
wget -q -O - "http://ftp.apnic.net/stats/apnic/delegated-apnic-latest" &gt; $new 2&gt; $errors

if [ $? -eq 0 ]; then  
    sort_new=`mktemp`
    sort_old=`mktemp`
    diff_out=`mktemp`

    # 日本国内IPについてのみ比較する
    /bin/grep '^apnic|JP|ipv4|' $new &gt; $sort_new
    /bin/grep '^apnic|JP|ipv4|' $list &gt; $sort_old
    diff --ignore-matching-lines="^#" $sort_new $sort_old &gt; $diff_out
    if [ $? -ne 0 ]; then
        (
         echo '-------------------- old delegated-apnic-latest --------------------'
         grep -P '^\d' $list
         echo
         echo '-------------------- new delegated-apnic-latest --------------------'
         grep -P '^\d' $new
         echo '---------------------- difference ----------------------'
         cat $diff_out

         # メールは rootにだす（ /etc/aliases に従う）
        ) | mail -s "delegated-apnic-latest updated $(hostname)" root
        cp -f $new $list

        # iptables の -s に渡せる形式に変換
        $DIR/jponly.pl &lt; $list &gt; $DIR/jponly.list

#        # rc.iptables を再実行する
#        \time $DIR/rc.iptables

         # iptables-save/restoreで更新する
         $DIR/update_mangle.pl
    fi
    rm -f $sort_new $sort_old $diff_out
else  
    cat $errors | mail -s "delegated-apnic-latest update check error $(hostname)" root
fi  
rm -f $new $errors  
</code></pre>

<p>このスクリプトは1日1回、APNICにお伺いを立てて（日本に割り当てられた）IPv4アドレスリストが変化していたら<code>/etc/rc.d/delegated-apnic-latest</code>ファイルを更新する。通知メールはrootに向けて投げるため、<code>/etc/aliases</code>に実際の通知先メールアドレスを書いておこう。</p>

<p>取得したリストの中身は概ね以下のような行の羅列である。</p>

<pre><code class="language-brush:plain">apnic|CN|ipv4|223.214.0.0|131072|20100803|allocated  
apnic|JP|ipv4|223.216.0.0|262144|20100712|allocated  
apnic|CN|ipv4|223.220.0.0|131072|20100723|allocated  
apnic|KR|ipv4|223.222.0.0|65536|20100721|allocated  
apnic|JP|ipv4|223.223.0.0|32768|20100803|allocated  
apnic|IN|ipv4|223.223.128.0|8192|20100809|allocated  
</code></pre>

<p>バーチカルラインで区切られた各カラムは次の意味を持つ。</p>

<ul>
<li>IP管理組織</li>
<li>2文字の国コード</li>
<li>IP種別</li>
<li>割り当てブロック先頭のIPアドレス</li>
<li>割り当てブロックに含まれるIP数（2の冪乗に一致）</li>
<li>割り当てられた日付</li>
<li>現在のステータス</li>
</ul>

<p>注意するのは5カラム目のIP数がIPマスクでもCIDRでもないという点で、ACLとして使用するにはこれをCIDRに変換する必要がある。これを行っているのが43行目の<code>jponly.pl</code>だ。</p>

<hr>

<h4 id="ipcidr">日本国内IPのCIDRを抽出する</h4>

<p>このフィルタスクリプトは、国コードがJPかつステータスがallocatedの行を抽出し、そのIP数からCIDRを計算して出力する。</p>

<pre><code class="language-brush:perl gutter:true title:/etc/rc.d/jponly.pl">#!/usr/bin/perl
use strict;  
use utf8;

###
### /etc/rc.d/jponly.pl
###
### delegated-apnic-latest から日本国内IPのCIDRを抽出する
###

my %CIDR;  
for my $bit ( 0..32 ) {  
    my $len = 2 ** ( 32 - $bit );
    $CIDR{$len} = $bit;
}
while ( &lt;&gt; ) {  
    chomp;
    if ( m{^apnic\|JP\|ipv4\|(\d+(?:\.\d+){3})\|(\d+)\|\d+\|allocated} ) {
        my $addr = $1;
        my $cidr = $CIDR{ $2 || 32 };
        printf "%s/%u\n", $addr, $cidr;
    }
}
exit 0;  
__END__  
</code></pre>

<p>これを通して生成した<code>/etc/rc.d/jponly.list</code>は1行1ブロックのIP/CIDRリストとなる。</p>

<pre><code class="language-brush:plain">223.216.0.0/14  
223.223.0.0/17  
</code></pre>

<hr>

<h4 id="iptablesacl">iptablesでのACL</h4>

<p>これを用いて例えばiptablesのmangleテーブル・Jponlyチェイン（フィルタ）に書き込むには次のようにする。</p>

<pre><code class="language-brush:bash futter:true">#!/bin/bash
JPONLY=/etc/rc.d/jponly.list

# mangleテーブルでフィルタを作る
iptables -t mangle -N Jponly  
cat $JPONLY | while read; do  
  iptables -t mangle -A Jponly -s $REPLY -j MARK --set-mark 2/2
done

# 上記のフィルタをINPUT/FORWARDに適用してパケットにmarkビットを立てる
iptables -t mangle -A INPUT -j Jponly  
iptables -t mangle -A FORWARD -j Jponly

# 他のチェインの中で、markビットが立っているならACCEPT、なければDROPする例
iptables -A Accept -m mark --mark 2/2 -j ACCEPT  
iptables -A Accept -j DROP  
</code></pre>

<p>mangleとmarkビットマスクを使うことで、このJponlyフィルタを容易に再利用できるようにしている。この他にホワイトリストチェイン・ブラックリストチェインを併用する場合も、markビットマスクを利用することで柔軟なACLフィルタを作り出すことが出来る。</p>

<hr>

<h4 id="">フィルタチェインの高速更新</h4>

<p><code>apnic.list_update</code>の49行目で呼び出している<code>update_mangle.pl</code>は、iptablesで設定した既存のJponlyチェインをアップデートするスクリプトだ。これはiptables-save・iptables-restoreコマンドを使用して高速に更新処理を行う。初回こそ前項のようにiptablesコマンドで丁寧に設定する必要があるが、これは数秒〜数十秒の処理時間が掛かってしまう。そこで2回目以降は該当チェインだけを書き換えることでダウンタイムを実用上問題ない時間（ミリ秒オーダー）まで切り詰める。<sup id="fnref:3"><a href="http://multix.jp/delegated-apnic-acl/#fn:3" rel="footnote">3</a></sup></p>

<pre><code class="language-brush:perl gutter:true title:/etc/rc.d/update_mangle.pl">#!/usr/bin/perl
use strict;

my $JPONLY  = '/etc/rc.d/jponly.list';  
my $UPDATE  = '/etc/rc.d/iptables.bak';  
my $SAVE    = '/sbin/iptables-save -c';  
my @RESTORE = ('/sbin/iptables-restore', '-c');

my $SEARCH   = qr{-A Jponly -s};  
my $TEMPLATE = "[0:0] -A Jponly -s %s -j MARK --set-xmark 0x2/0x2 \n";

my $BEFORE;  
my $AFTER = [];  
my @INSERT;

my $PIPE;  
open $PIPE, '&lt;', $JPONLY or die;  
while (&lt;$PIPE&gt;) {  
    chomp;
    push @INSERT, sprintf $TEMPLATE, $_;
}

close $PIPE;  
open $PIPE, '-|', $SAVE or die;  
while (&lt;$PIPE&gt;) {  
    unless ($BEFORE) {
        if ($_ =~ $SEARCH) {
            $BEFORE = $AFTER;
            push @$BEFORE, @INSERT;
            next;
        }
    }
    elsif ($_ =~ $SEARCH) {
        next;
    }
    push @$AFTER, $_;
}
close $PIPE;

open $PIPE, '&gt;', $UPDATE or die;  
print $PIPE @$BEFORE;  
print $PIPE @$AFTER;  
close $PIPE;

system @RESTORE, $UPDATE;

1;  
__END__  
</code></pre>

<hr>

<h4 id="apacheacl">ApacheでのACL</h4>

<p>前述の<code>jponly.list</code>からApache用のACLを作り出すには次のようにする。</p>

<pre><code class="language-brush:bash">#!/bin/bash
JPONLY=/etc/rc.d/jponly.list

( cat $JPONLY | while read; do
  echo "Allow from $REPLY"
done ) &gt; /etc/httpd/conf/jponly.conf  
</code></pre>

<p>こうして生成したACLを、必要な場所でIncludeディレクティブを使用して読み込む。なおリストが更新されたらhttpdプロセスをreload(あるいはgraceful)しなければ実際には反映されない。<sup id="fnref:4"><a href="http://multix.jp/delegated-apnic-acl/#fn:4" rel="footnote">4</a></sup></p>

<pre><code class="language-brush:plain">Order Allow,Deny  
Allow from localhost  
Allow from 192.168.0.0/16  
Include conf/jponly.conf  
Deny from All  
</code></pre>

<hr>

<h4 id="nginxacl">nginxでのACL</h4>

<p>nginxでACLを掛けるには、geoモジュールを使用する。まずApacheの場合と同様にInclude可能なファイルを作成する。</p>

<pre><code class="language-brush:bash">#!/bin/bash
JPONLY=/etc/rc.d/jponly.list

( cat $JPONLY | while read; do
  echo "$REPLY 2;"
done ) &gt; /etc/nginx/jponly.conf  
</code></pre>

<p>これをhttpブロックのgeoモジュールで読み込み、$acl変数に反映させる。これをlocationブロック内で参照して条件分岐する。なおリスト更新後はnginxプロセスをreloadしなければ実際には反映されない。</p>

<pre><code class="language-brush:plain">http {  
    geo $acl {
        default        0;
        127.0.0.1      1;
        192.168.0.0/16 1;
        include jponly.conf;
    }
    server {
        location /jponly/ {
            root /var/www/html_jponly;
            if ($acl = 0) {
                return 403;
            }
        }
        location / {
            root /var/www/html_public;
        }
    }
}
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>このリストにはIPv6も記載されているし、日本以外のアジア諸国もすべて入っている。特定の国のIPだけ弾きたい、といったニーズならばそれに応じたフィルタを書けば同じように対応できる。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>あるときA国で使われていたIPが、気がついたら何時の間にかB国に再割当てされていたということは割とある。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>差分更新処理は端折っているため、実行すると各IPブロックのカウンターはすべてゼロ初期化される。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>GeoIPモジュール <a href="http://dev.maxmind.com/geoip/">http://dev.maxmind.com/geoip/</a> を使ったほうが手間は少ないし処理も高速だし設定もスッキリするが、DBファイルをアップデートしたらサービスリロードが必要になるのは変わらない。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:4" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item></channel></rss>