技術メモ

神奈川在住のITエンジニアの備忘録。おもにプログラミングやネットワーク技術について、学んだことを自分の中で整理するためにゆるゆると書いています。ちゃんと検証できていない部分もあるのでご参考程度となりますが、誰かのお役に立てれば幸いです。

OIDのBER形式でのエンコード

以前、以下の記事のコードを書いた時、SNMPのOIDの BER(Basic Encoding Rule)形式でのエンコード処理について勉強した。

akrad.hatenablog.com

今回は、その時に勉強した内容を書く。

 

SNMPのOIDのエンコードで面倒なのは、OIDの数値が128以上の場合である。というのは、OIDの数値が128以上の場合、1バイトではなく複数バイトで表現することになっており、それら複数バイトの最後の1バイト以外は先頭bitを立てる必要があるからである。文章だけでは分かりにくいと思うので、例を挙げて説明する。

例えば、OIDの中に 9999 という数値があったとする。まず、128 進数として分解すると以下になる。

9999 = (128^1 * 78) + (128^0 * 15

 

エンコードする際、最後の1バイト以外は先頭ビットを立てるので、以下にようになる。(見やすくするために変換結果は16進で表現している。)

78 = 0100 1110‬--(先頭bitを立てる)--> 1100 1110 = 0xCE
15 = 0x0F

以上により、9999 のエンコード結果は CE0F となる。

複数バイトに分けた際、どこまでが一纏まりなのか判別するために上記の仕様になっていると考えている。

 

また、OIDの先頭部分は、「最初のOIDの数値×40 + 二つ目のOIDの数値」として1バイトに纏めることになっているので、例えば、OIDの先頭が .1.3 だったら、1×40+3=43 となり、16進では 2B となる。最初の数値と二つ目の数値についてこうした変換をしている理由は分かっていないが、おそらく1バイトに纏めて通信量を減らすためだと思われる。

 

以上を踏まえると、例えば、「.1.3.6.1.4.1.9999」というOIDをBER形式でエンコードすると、 「2B 06 01 04 01 CE 0F」となる。

16進のSNMP OIDを通常の文字列の形式に変換する

主題の処理を行うperlスクリプト convertOidHexToStr.pl を作成した。使い方は、例えば、「perl convertOidHexToStr.pl 2b06010401CE0F」 や 「perl convertOidHexToStr.pl "2b 06 01 04 01 CE 0F"」 のように指定すると、結果として、人に優しい形式でOID文字列を「.1.3.6.1.4.1.9999」と出力する。

use strict;
use warnings;

# -----------------------
# Check args.
# -----------------------
my $oidHex = $ARGV[0];
$oidHex =~ s/\s//g;
if (length($oidHex)%2 != 0 or $oidHex =~ /[^0-9a-fA-F]/) {
    die "Input is not hex string\n";
}

my @hexUnits = $oidHex =~ /.{2}/g;
my $length = @hexUnits;

# -----------------------
# Main.
# -----------------------
my $oidStr;
my @tmpsubIds;
my $isInMultiBytes = 0;
my $subIdCnt = 0;
for(my $i=0; $i<$length; $i++) {
    my $subId = hex($hexUnits[$i]);
    if ($i == 0) {
        my $firstSubId = int($subId / 40);
        my $secondSubId = $subId % 40;
        if ($firstSubId > 2) {
            die "The first sub id is 0 or 1 or 2.\n";
        }
        $oidStr .= "\." . $firstSubId . "\." . $secondSubId;
        $subIdCnt += 2;
        next;
    }
    
    if ($subId >= 128) {
        $subId -= 128;
        push(@tmpsubIds, $subId);
        $isInMultiBytes = 1;
    }
    elsif ($isInMultiBytes == 1) {
        push(@tmpsubIds, $subId);
        my $subIdForMultiBytes = computeSubIdForMultiBytes(@tmpsubIds);
        $subIdCnt++;
        if (isSubIdCntOverMax($subIdCnt)) {
            die "Sub id count is over the max value(128).\n";
        }
        $oidStr .= "\." . $subIdForMultiBytes;
        @tmpsubIds = ();
        $isInMultiBytes = 0;
    }
    else {
        if (isSubIdOverMax($subId)) {
            die "Sub id is over the max value(4294967295).\n";
        }
        $subIdCnt++;
        if (isSubIdCntOverMax($subIdCnt)) {
            die "Sub id count is over the max value(128).\n";
        }
        $oidStr .= "\." . $subId;
    }
}

print $oidStr;

# -----------------------
# Sub functions.
# -----------------------
sub isSubIdCntOverMax {
    my $subIdCnt = shift;
    if ($subIdCnt > 128) { # According to rfc2578.
        return 1;
    }
    return 0;
}

sub isSubIdOverMax {
    my $subId = shift;
    if ($subId > 4294967295) { # over 2^32-1. According to rfc2578.
        return 1;
    }
    return 0;
}

sub computeSubIdForMultiBytes {
    my @subIds = @_;
    my $length = @subIds;
    my $computedSubId;
    for(my $i=0; $i<$length; $i++) {
        $computedSubId += (128**($length-$i-1)) * $subIds[$i];
    }
    return $computedSubId;
}


パケットダンプを読むとき、
akrad.hatenablog.com
で書いたようにちょっと面倒なところがあるので、このスクリプトを作成した。

ダイレクトブロードキャスト

今日、ダイレクトブロードキャストなるものを初めて知った。ブロードキャストというと、同一ネットワーク内に一斉送信されるものという認識であったが、このダイレクトブロードキャストは異なるネットワーク内に対して一斉送信されるものである。

 

あまり使用されないが、ネットワーク越しにコンピュータの電源を入れる「Wake-on-LAN」で使われることがあるらしい。Wake-on-LAN の概要は以下。

Wake-on-LAN - Wikipedia

 

なお、ダイレクトブロードキャストは、セキュリティ上の理由から、通常はルータでドロップされる設定になっているようだ。

サマータイムで影響を受けるシステム

最近、東京五輪に関連して話題に挙がっているサマータイムの導入。「システムが影響を受ける!」という声が多方面から挙がっているが、システムが影響を受けるケースについて、ネット上の情報をもとに簡単に纏めてみた。

 

  • WindowsLinux等のOSは、内部時間をUTC(世界標準時)で持っていて、その値はタイムゾーンによって変わらない。(OSの時刻表示は、上記の内部時間をタイムゾーンに合わせて加算/減算して表示しているだけ)
  • サマータイムが適用されても、OSの内部時間は変わらない。OSの内部時間がタイムゾーン:JDT(Japan Daylight Saving Time) に合わせて、OSの「時刻表示」が変わるだけ。
  • なので、OS上のシステムが時刻を内部時刻(UTC)で扱っていれば、サマータイムによる時刻の戻り・進みの影響は受けないと思われる。
  • 一方で、システムが時刻をローカルタイム(=OSの表示時刻)で扱っていると、サマータイムによって時刻が戻ったり飛んだりするので、影響を受ける可能性がある。

 

つまり、システム開発において、時刻を取得する際にOSの内部時間(UTC)で扱うAPIを使っていれば、サマータイムが導入されても影響は受けないのではないか。

ただ、東京五輪までに、全てのシステムについて時刻を扱う処理を見直し、時刻をローカルタイムで扱っている箇所を見つけて改修するのは、巷で言われているようにかなり困難であると思う。。

SNMP trap (coldStart) 送信スクリプト

以前作成した以下の perl スクリプトをエンハンスした。
akrad.hatenablog.com

具体的には、SNMPv1 のcoldStartトラップも SNMPv2cのcoldStartトラップも送信できるようにし、トラップ送信先やSNMPv1トラップの agent-addrフィールドも指定できるようにした。
使い方の例は以下。


1.1.1.1 に、SNMPv2cのcoldStartトラップを送信する。

perl sendTrap.pl -v 2 -d 1.1.1.1


1.1.1.1 に、agent-addrフィールドを 2.2.2.2 として、SNMPv1のcoldStartトラップを送信する。

perl sendTrap.pl -v 1 -d 1.1.1.1 -a 2.2.2.2


作成したperlスクリプトは以下の通り。
sendTrap.pl

use strict;
use warnings;
use Socket;
use Getopt::Long 'GetOptions';

#---------------
# Constant value
#---------------
my $V1_TRAP   = 'a4';
my $V2c_TRAP  = 'a7';
my $SNMP_V1   = 0;
my $SNMP_V2C  = 1;

my $INTEGER      = '02';
my $OCTET_STRING = '04';
my $OBJECT_ID    = '06';
my $SEQUENCE     = '30';
my $IP_ADDRESS   = '40';
my $TIME_TICKS   = '43';

#-------------------
# Set default value
#-------------------
my $destAddr      = 'localhost';
my $destPort      = 162;
my $snmpVersion   = '2';
my $enterpriseOid = '1.2.3.4.5';
my $agentAddr     = '127.0.0.1';

GetOptions(
    'dest=s'      => \$destAddr,
    'version=s'   => \$snmpVersion,
    'agentaddr=s' => \$agentAddr
);

#-------------------
# Check user's input
#-------------------
if ($destAddr =~ /^\d/) { # This is an ip address. Because hostname doesn't start from a digit.
    if (!isIpAddress($agentAddr)) {
        die("$destAddr is not an ip address.");
    }
}

if (($snmpVersion ne '1') and ($snmpVersion ne '2') and ($snmpVersion ne '2c')) {
    die("$snmpVersion must be 1 or 2(2c)");
}

if (!isIpAddress($agentAddr)) {
    die("$agentAddr is not an ip address.");
}


#---------------
# MAIN
#---------------
my $socket;
socket($socket, PF_INET, SOCK_DGRAM, 0) or die "Failed to create socket. $!";

my $addrBin = inet_aton($destAddr);
my $sockAddr = pack_sockaddr_in($destPort, $addrBin);

my $trapData;
if ($snmpVersion eq '1') {
    $trapData = constractV1TrapData();
}
elsif (($snmpVersion eq '2') or ($snmpVersion eq '2c')) {
    $trapData = constractV2cTrapData();
}
else {
    die("Need to specify 1 or 2(2c) for version.");
}

my $trapDataBin = pack("H*", $trapData);
send($socket, $trapDataBin, 0, $sockAddr) or die "Failed to send. $!";


#---------------
# Sub functions
#---------------
sub constractV1TrapData {
    my $snmpVersion  = $SNMP_V1;
    my $commName     = 'public';
    my $pduType      = $V1_TRAP;
    my $requestID    = 10;
    my $errorStatus  = 0;
    my $errorIndex   = 0;
    my $genericTrap  = 0;
    my $specificTrap = 0;
    my $timeStamp    = 0;

    my $snmpVersionBerUnit   = constractBerData($INTEGER, convertDecNumToHexStr($snmpVersion));
    my $commNameBerUnit      = constractBerData($OCTET_STRING, unpack("H*", $commName));
    my $enterpriseOidBerUnit = constractBerData($OBJECT_ID, convertOidStrToHexStr($enterpriseOid));
    my $agentAddrBerUnit     = constractBerData($IP_ADDRESS, convertIpAddrStrToHexStr($agentAddr));
    my $genericTrapBerUnit   = constractBerData($INTEGER, convertDecNumToHexStr($genericTrap));
    my $specificTrapBerUnit  = constractBerData($INTEGER, convertDecNumToHexStr($specificTrap));
    my $timeStampBerUnit     = constractBerData($TIME_TICKS, convertDecNumToHexStr($timeStamp));
    my $emptyVarbindBerUnit  = constractBerData($SEQUENCE, "");
    
    my $trapPduLength = getBinLengthFromHexStr(
                            $enterpriseOidBerUnit,
                            $agentAddrBerUnit,
                            $genericTrapBerUnit,
                            $specificTrapBerUnit,
                            $timeStampBerUnit,
                            $emptyVarbindBerUnit);
    
    my $totalength = getBinLengthFromHexStr(
                        $snmpVersionBerUnit, 
                        $commNameBerUnit, 
                        $pduType,
                        convertDecNumToHexStr($trapPduLength))
                     + $trapPduLength;

    my $trapData = $SEQUENCE . 
                   convertDecNumToHexStr($totalength) .
                   $snmpVersionBerUnit . 
                   $commNameBerUnit . 
                   $pduType .
                   convertDecNumToHexStr($trapPduLength) .
                   $enterpriseOidBerUnit .
                   $agentAddrBerUnit .
                   $genericTrapBerUnit .
                   $specificTrapBerUnit .
                   $timeStampBerUnit .
                   $emptyVarbindBerUnit;
    return $trapData;
}

sub constractV2cTrapData {
    my $snmpVersion = $SNMP_V2C;
    my $commName    = 'public';
    my $pduType     = $V2c_TRAP;
    my $requestID   = 10;
    my $errorStatus = 0;
    my $errorIndex  = 0;

    my $snmpVersionBerUnit = constractBerData($INTEGER, convertDecNumToHexStr($snmpVersion));
    my $commNameBerUnit    = constractBerData($OCTET_STRING, unpack("H*", $commName));
    my $requestIDBerUnit   = constractBerData($INTEGER, convertDecNumToHexStr($requestID));
    my $errorStatusBerUnit = constractBerData($INTEGER, convertDecNumToHexStr($errorStatus));
    my $errorIndexBerUnit  = constractBerData($INTEGER, convertDecNumToHexStr($errorIndex));
    my $varbindData        = constractVarbindData();
    
    my $trapPduLength = getBinLengthFromHexStr(
                            $requestIDBerUnit,
                            $errorStatusBerUnit,
                            $errorIndexBerUnit,
                            $SEQUENCE,
                            convertDecNumToHexStr(getBinLengthFromHexStr($varbindData)),
                            $varbindData);
    
    my $totalength = getBinLengthFromHexStr(
                        $snmpVersionBerUnit, 
                        $commNameBerUnit, 
                        $pduType,
                        convertDecNumToHexStr($trapPduLength))
                     + $trapPduLength;

    my $trapData = $SEQUENCE . 
                   convertDecNumToHexStr($totalength) .
                   $snmpVersionBerUnit . 
                   $commNameBerUnit . 
                   $pduType .
                   convertDecNumToHexStr($trapPduLength) .
                   $requestIDBerUnit .
                   $errorStatusBerUnit .
                   $errorIndexBerUnit .
                   $SEQUENCE .
                   convertDecNumToHexStr(getBinLengthFromHexStr($varbindData)) .
                   $varbindData;
    return $trapData;
}

sub constractVarbindData {
    # ------------------------
    # sysUpTime
    # ------------------------
    my $sysUpTimeOid = '1.3.6.1.2.1.1.3.0';
    my $sysUpTimeOidBerData = constractBerData($OBJECT_ID, convertOidStrToHexStr($sysUpTimeOid));
    my $sysUpTime = 10;
    my $sysUpTimeBerData = constractBerData($TIME_TICKS, convertDecNumToHexStr($sysUpTime));
    my $sysUpTimeVarbindLength = getBinLengthFromHexStr($sysUpTimeOidBerData, $sysUpTimeBerData);
    my $sysUpTimeVarbind = $SEQUENCE . 
                           convertDecNumToHexStr($sysUpTimeVarbindLength) . 
                           $sysUpTimeOidBerData .
                           $sysUpTimeBerData;

    # ------------------------
    # sysTrapOID
    # ------------------------
    my $sysTrapOid = '1.3.6.1.6.3.1.1.4.1.0';
    my $sysTrapOidBerData = constractBerData($OBJECT_ID, convertOidStrToHexStr($sysTrapOid));
    my $sysTrapOidData = '1.3.6.1.6.3.1.1.5.1';
    my $sysTrapBerData = constractBerData($OBJECT_ID, convertOidStrToHexStr($sysTrapOidData));
    my $sysTrapVarbindLength = getBinLengthFromHexStr($sysTrapOidBerData, $sysTrapBerData);
    my $sysTrapVarbind = $SEQUENCE . 
                         convertDecNumToHexStr($sysTrapVarbindLength) . 
                         $sysTrapOidBerData .
                         $sysTrapBerData;
    
    return $sysUpTimeVarbind . $sysTrapVarbind;
}

sub isIpAddress {
    my $ipAddr = shift;
    my @octets = split(/\./, $ipAddr);
    return 0 if (@octets != 4);
    foreach my $octet (@octets) {
        if (!($octet =~ /^\d+$/) or !(0 <= $octet and $octet <= 255)) {
            return 0;
        }
    }
    return 1;
}

sub convertIpAddrStrToHexStr {
    my $ipAddrStr = shift;
    my @octets = split(/\./, $ipAddrStr);
    my $ipAddrHexStr;
    foreach my $octet (@octets) {
        $ipAddrHexStr .= convertDecNumToHexStr($octet);
    }
    return $ipAddrHexStr;
}

sub convertOidStrToHexStr {
    my $oidStr = shift;
    my @oids = split(/\./, $oidStr);
    my $hexStr = convertDecNumToHexStr(40*$oids[0] + $oids[1]);
    for (my $i = 2; $i <= $#oids; $i++) {
        if ($oids[$i] < 128) {
            $hexStr .= convertDecNumToHexStr($oids[$i]);
        }
        else {
            $hexStr .= convertOidNumToHexStrForMultiBytes($oids[$i]);
        }
    }
    return $hexStr;
}

sub convertOidNumToHexStrForMultiBytes {
    my $oidNum = shift;
    my $hexStr = "";
    my $counter = 0;

    my $workVal = $oidNum;
    while (1) {
        my $quotient  = int($workVal / 128);
        my $remainder = $workVal % 128;

        if ($counter != 0) {
            $remainder += 128;
        }
        $hexStr = convertDecNumToHexStr($remainder) . $hexStr;

        if ($quotient == 0) {
            last;
        }
        $workVal = $quotient;
        $counter++;
    }
    return $hexStr;
}

sub getBinLengthFromHexStr {
    my @hexStrs = @_;
    my $binLength = 0;
    foreach my $hexStr (@hexStrs) {
       $binLength += length($hexStr)/2;
    }
    return $binLength;
}

sub constractBerData {
    my ($tag, $data) = @_;
    $data =~ s/\s//g;
    my $binLength = (length($data))/2;
    my $berData = $tag . convertDecNumToHexStr($binLength) . $data;
    return $berData;
}

sub convertDecNumToHexStr {
    my $decNum = shift;
    my $hexStr = sprintf("%02x", $decNum);
    if (length($hexStr)%2 != 0) {
        $hexStr = '0' . $hexStr;
    }
    return $hexStr;
}


上記のスクリプト作成に当たっては、OID を BER(Basic Encoding Rules) 形式でエンコーディングするのがちょっと面倒だった。(convertOidStrToHexStr関数やconvertOidNumToHexStrForMultiBytes関数の辺り・・)
この辺の話については、別途、記事を書きたいと思っている。

AOSSを使って、プリンタと無線LANルータを接続

自宅で、無線LANルータ(buffalo)経由で PCとプリンタを接続するために、プリンタと無線LANルータを無線で接続することになった。簡単に、構成は以下。

 

無線LANルータ

          |

          +---------(ここは接続済)--------- PC

          |

          + ------(接続したのはここ)------ プリンタ

 

PCやスマホから無線LANルータへの接続は何度もやったことがあるので慣れているが、プリンタから無線LANルータはやったことがなかったので、どうやってやるのかな・・と思って調べたところ、AOSS (AirStation One-Touch Secure System) という便利な技術があることが分かった。この技術を使うと、無線LANルータのSSIDやPWを手入力せずに接続設定を行うことができる。

幸い、プリンタも無線LANルータもAOSSに対応していたので、以下の手順により接続を行うことができた。

  1. プリンタのAOSSを起動する。(プリンタによって手順は異なる)
  2. 無線LANルータのAOSSを起動する。(無線LANルータ側のAOSSボタンを押す)

これだけで、プリンタと無線LANルータの接続設定が完了し、PCから無線LANルータ経由でプリンタを操作できるようになった。

SNMPのパケットダンプを解析する時のコツ

先ほど、

SNMP v2c trap (coldStart) 送信スクリプト - Akira's Blog

の記事を書くために SNMPトラップのパケットダンプを解析した。この際に SEQUENCE型を示す 0x30 に着目すると、各データの区切り位置が分かって解析しやすかった。

 パケットダンプ内に 0x30 があったとして、それが SEQUENCE型を示すとは限らないが、目安として 0x30 をデータの区切り位置と見なすと、パケット解析が捗りそうな気がする。

ちなみに、解析したパケットダンプは以下。

30 40 02 01 01 04 06 70 75 62 6c 69 63 a7 33 02 
01 0a 02 01 00 02 01 00 30 28 30 0d 06 08 2b 06 
01 02 01 01 03 00 43 01 0a 30 17 06 0a 2b 06 01 
06 03 01 01 04 01 00 06 09 2b 06 01 06 03 01 01 
05 01

 ここに登場する 0x30 は全てSEQUENCE型を示すもので、以下の通り、データの区切りと見なせる。

30 : SEQUENCE     # トラップデータの開始
40 : Total length
02 01 01 : SNMP version
04 06 70 75 62 6c 69 63 : community name
a7 : V2-TRAP-PDU
33 : PDU length
02 01 0a : request id (10)
02 01 00 : error status
02 01 00 : error index

30 : SEQUENCE     # varbindリストの開始
28 : varbind list length

30 : SEQUENCE     # varbind(1つ目)の開始
0d : varbind length
06 08 2b 06 01 02 01 01 03 00 : .1.3.6.1.2.1.1.3.0
43 01 0a : sysUpTime value (10)

30 : SEQUENCE
17 : varbind length     # varbind(2つ目)の開始
06 0a 2b 06 01 06 03 01 01 04 01 00 : .1.3.6.1.6.3.1.1.4.1.0
06 09 2b 06 01 06 03 01 01 05 01 : .1.3.6.1.6.3.1.1.5.1

 

今後、SNMPデータのパケットダンプを解析する時は、0x30 を区切りの目安として考えたい。

まあ、wiresharkとか使える環境だったら、使った方が断然楽で速いのだが、そうでない環境で作業することもあると思うので、今回纏めてみた。