C#でSNMPのGetNextメッセージをエンコードする

# 概要

[C#でASN.1のObject Identifierのエンコードを行う](https://qiita.com/sukkyxp/items/69e142d07aa92aaa09c2)の続編です。
前回は、ASN.1のObject Identifierのエンコード、TLVのLengthのエンコードについて実装しました。
今回は、SNMPのGetNextRequestメッセージのエンコードに必要な整数、OCTET STRING、NULL、SEQUENCEについてのエンコードを実装します。
最後に、SNMPのGetSnmpRequestメッセージを作成するクラスを作成します。

# NullValueのエンコード

ASN.1のNull ValueはTAG番号が「0x05」でLengthは常に「0」です。
よって、常に「05 00」です。

よって、NullValueのエンコードを行うクラスの実装は下記のようになります。

```C#
    public class NullValue : AsnValue
    {
        public NullValue() { }

        public override byte GetTag()
        {
            return 0x05;
        }

        public override List<byte> GetBytes()
        {
            return new byte[] { GetTag(), 0x00 }.ToList();
        }

    }
```

#Octet Stringのエンコードクラスの実装。

Octet StringはASN.1のTAG番号は「0x04」です。
Lengthは、Valueの長さになります。
また、OCTET STRINGは今回はSNMPv1のCommunity名の設定に使用しますので、
ASCII文字列をValueとして保持することのみ想定していますが、OCTET STRINGは文字列以外に、
バイナリストリームを設定する場合にも使用されます。
バイナリストリームをエンコードする場合はコンストラクタでバイナリストリームやバイト配列を
受け取るように変更すれば良いでしょう。

```C#
    public class OctestStringValue : AsnValue
    {
        public String OctestString {get; set;}

        public OctestStringValue(String value)
        {
            this.OctestString = value;
        }
        public override List<byte> GetBytes()
        {
            List<byte> result = new List<byte>();
            result.Add(GetTag());

            byte[] data = Encoding.ASCII.GetBytes(OctestString);

            result.AddRange(GetFieldLengthByte(data.Length));
            result.AddRange(data.ToList());

            return result;
        }

        public override byte GetTag()
        {
            return 0x04;
        }
    }
```

#整数のエンコードについて

今回の最難関は整数のエンコードです。
整数は負数を2の補数で表します。またBigEndianです。
注意が必要なのは、C#でInt32やInt64などをByte配列にBitConverter.GetBytesで変換すると
余分な「00」のバイトや「FF」のバイトが付加されてしまうことです。
ASN.1のBERでは最小バイトすうとなるように、先頭から連続する9桁の0や連続する9桁の1を設定することが
認めれていません。

例えば、
「12345」->「39 30 00 00」
「-12345」->「C7 CF FF FF」
となります。
リトルエンディアンなので、リバースしてそれぞれ、
「00 00 30 39」「FF FF CF C7」です。
正数の場合は先頭の「00」が連続する箇所を取り除くだけでも良さそうですが、負数の場合に先頭の「FF」を
単純に取り除くと値が変わってしまいます。

そこで、C#の、System.Numeric.BigIntegerを今回は使用することとしました。
BigIntegerは非常に大きな整数を扱うクラスですが、BigInteget.ToByteArray()で最小のバイト数のバイト配列への変換が可能でした。
これがなかったら自力で大きな正数、負数を最小のバイト数のバイト配列に変換するアルゴリズムを考える必要があり、悩んでしましたが、BigIntegerのおかげで、自分で考える機会がを奪われました。

```C#
    public class IntegerValue : AsnValue
    {
        public BigInteger Value { get; set; }

        public IntegerValue(BigInteger value)
        {
            this.Value = value;
        }
        public override List<byte> GetBytes()
        {
            List<Byte> result = new List<byte>();
            byte[] data = Value.ToByteArray();
            result.Add(GetTag());
            result.AddRange(GetFieldLengthByte(data.Length));
            result.AddRange(data.Reverse().ToList());

            return result;
        }

        public override byte GetTag()
        {
            return 0x02;
        }
    }
```

なお、BigIntegerを使用するにはプロジェクトの参照設定で「System.Numerics」ライブラリの参照が
必要です。
System.Numericsへの参照を追加した上で、Classファイルの冒頭に
「using System.Numerics;」の追加が必要です。

#SEQUENCEのエンコード

シーケンスは内部に複数の単純型のTLVの値を保持する復号型です。
TAG番号は「0x30」です。Lengthは保持する複数のTLVの実際のバイト数です。

```C#
    public class SequenceValue : AsnValue
    {

        public List<AsnValue> AsnValues { get; set; }

        public SequenceValue()
        {
            AsnValues = new List<AsnValue>();
          
        }
        
        public override List<byte> GetBytes()
        {
            List<byte> result = new List<byte>();

            foreach (AsnValue value in AsnValues)
            {
                result.AddRange(value.GetBytes());
            }
            result.InsertRange(0, GetFieldLengthByte(result.Count()));
            result.Insert(0, GetTag());

            return result;

        }

        public override byte GetTag()
        {
            return 0x30;
        }
    }
```

#続いてSNMPに特化した値のエンコードクラスを作成します。

SNMPのデータ部には、「GetNextRequest」「GetRequest」「GetRespons」などの種類がありますが、
それぞれTAG番号が異なるだけで、全てSEQUENCEとエンコード方法は同じです。

```C#
    public class SnmpDataValue : SequenceValue
    {
        public enum SnmpDataType
        {
            GetRequest,
            GetNextRequest,
            GetResponse,
        };

        public SnmpDataType DataType {get; set;}

        public SnmpDataValue(SnmpDataType dataType) : base()
        {
            this.DataType = dataType;
        }

        public override byte GetTag()
        {
            switch (DataType) {
                case SnmpDataType.GetRequest:
                    return 0xA0;
                case SnmpDataType.GetNextRequest:
                    return 0xA1;
                case SnmpDataType.GetResponse:
                    return 0xA2;
                default:
                    return 0x00;
            }
            
        }
    }
```

#SNMPメッセージクラス

いよいよこれまでに作成したクラスを組み合わせてSNMPのメッセージ全体をエンコードするクラスを作成します。
SNMPのGetNextRequestメッセージをエンコードするクラスを作成しました。
SNMPのメッセージ構造に従ってメッセージの階層を作成しています。
なお、RFCなどの仕様書を見ながらではなく、CentOSのsnmpgetnextコマンドで送信時にパケットをキャプチャし、
取得したパケットを見ながら実装して行きます。

![SNMPMESSAGE.png](https://qiita-image-store.s3.amazonaws.com/0/197947/4a9e77e2-0683-b541-e63f-e53ae28dbfee.png)



```C#
    public class SnmpGetNextMessage : SequenceValue
    {
        private Int32 version = 0; // SNMP V1

        public SequenceValue RequestBindings { get; set; }

        private SnmpDataValue snmpData;

        private const Int32 ERROR_STATUS = 0;
        private const Int32 ERROR_INDEX = 0;

        public SnmpGetNextMessage(String communityName)
        {
            AsnValues.Add(new IntegerValue(version));
            AsnValues.Add(new OctestStringValue(communityName));

            snmpData = new SnmpDataValue(SnmpDataValue.SnmpDataType.GetNextRequest);
            int requestId = new Random().Next();
            snmpData.AsnValues.Add(new IntegerValue(requestId));
            snmpData.AsnValues.Add(new IntegerValue(ERROR_STATUS));
           
            snmpData.AsnValues.Add(new IntegerValue(ERROR_INDEX));
            RequestBindings = new SequenceValue();
            snmpData.AsnValues.Add(RequestBindings);
            AsnValues.Add(snmpData);
        }

        public void AddRequestOid(String oid)
        {
            SequenceValue addOid = new SequenceValue();
            addOid.AsnValues.Add(new ObjectIdentifierValue(oid));
            addOid.AsnValues.Add(new NullValue());
            RequestBindings.AsnValues.Add(addOid);

        }


    }
```

#使い方

今回はレスポンスの取得をしていないため、実際に送信できているかどうかは、パケットキャプチャソフト(Wireshark）などを使って確認ができます。

OIDとIPアドレスを入力するテキストボックスとボタンだけのフォームを作成します。
txtOriginal.Text　に「1.3.6.1.2.1.1.5」
txtIp.Textに「1.2.3.4」
などを入力します。
なお、自アドレスを指定してもWiresharkではパケットの取得ができません。
UDPですので接続相手がいなくてもパケットはとりあえずは送信されます。
SnmpGetNextMessageのコンストラクタで渡しているのはコミュニティ名のデフォルト値の「public」です。

```C#
            SnmpGetNextMessage getNextMessage = new SnmpGetNextMessage("public");
            getNextMessage.AddRequestOid(txtOriginal.Text);

            string remoteHost = txtIp.Text;
            int remotePort = 161;

            System.Net.Sockets.UdpClient udp = new System.Net.Sockets.UdpClient();

            byte[] sendData = getNextMessage.GetBytes().ToArray();

            udp.Send(sendData, sendData.Length, remoteHost, remotePort);

            udp.Close();
```

sendDataをBitConverter.ToString().Replace("-"," ")でも16進文字列に変換すると、下記の通りとなります。
RequestId部分はランダムな整数値を設定しているため実行環境によって異なる値が入っていますが、それ以外は同じ値が出力されるかと思います。

```HEX
30 28 02 01 00 04 06 70 75 62 6C 69 63 A1 1B 02 04 65 C4 8D A4 02 01 00 02 01 00 30 0D 30 0B 06 07 2B 06 01 02 01 01 05 05 00
```

先頭から目でデコードしていくと、
「30 28」は、SEQUENCEで、40byte(0x28)のデータ。
「02 01 00」は整数で値は「0」
「04 06 70 75 62 6C 69 63」は6byteのOCTET STRINGでASCII変換すると「public」の文字列。
「A1 1B」はSNMPのgetNextRequestでデータ帳は27byte。
「02 04 65 C4 BD A4」は整数で値は「‭1707392420‬」（RequestID)
「02 01 00」は整数で0(ErrorStatus)
続く「02 01 00」も整数で0(Error Index)
「30 0D」はSEQUENCEでデータ部が13byte。
「30 0B」はSEQUENCEでデータ部が11byte。(SEQUENCEが入れ子になっています）
「06 07 2B 06 01 02 01 01 05」はObject Identifierで「1.3.6.1.2.1.1.5」
最後の「05 00」はNullValue。
となっていることが分かります。
上記のデータをUDP送信し、Wiresharkでそのパケットを取得してSNMPのプロトコルとして正しく解析されていれば、
OKかと思います。


#最後に
エンコードの実装により送信メッセージの作成までは出来るようになりました。
次回は、受信メッセージのデコードの実装を行います。

###続きはこちらです

[「C#でSNMPメッセージのデコーディングの実装のためのSNMP受信メッセージの見える化を行う」](https://qiita.com/sukkyxp/items/4438a40d2bab033939dc)