はじめに
NETCONFに対応したNW装置に、自作のGoプログラムから設定を入れたいと思うこともありますよね?
今回は github.com/Juniper/go-netconf/netconf
を使用して、Go言語でのNETCONFによるサンプルコードを作成し、vJunos-Switchを制御する実現性と出力を確認します。
環境
- vJunos-Switch
- mgmt IP: 10.100.2.94
- ssh username: qiita
- ssh password: qiita-password
- NETCONFが可能なように設定済み
- 上記にアクセス可能な適当な開発端末
vJunos Switchは以下を参考に各自で構築するか、余っているNW装置をご利用ください。
Go言語の開発環境に関しての話は省略しています
サンプルコード
まずは簡単にNETCONFでインターフェース ge-0/0/0
のMTUを1400に変更するコードを書きます。
1行1行のコード解説は省略しますが、大まかな処理内容は以下の通りです。
- SSH/NETCONFでNW装置に接続
- 意図しない設定の混入を防ぐため、作業中の変更を破棄
- 占有状態で設定モードに移行
- ge-0/0/0のMTU変更の設定を追加
- commit前の差分確認
- commit
NW装置に慣れた人であれば set interfaces ge-0/0/0 mtu 1400
をコミットするだけのコードだと説明したら伝わるでしょうか。そう考えるとプログラムとは長いものですね。
package main
import (
"fmt"
"log/slog"
"github.com/Juniper/go-netconf/netconf"
"golang.org/x/crypto/ssh"
)
const (
rpcCommit = "<commit/>"
rpcDiscard = "<discard-changes/>"
rpcGetCandidateCompare = "<get-configuration compare=\"rollback\" rollback=\"%d\" format=\"text\"/>"
rpcConfigLoadText = "<load-configuration action=\"merge\" format=\"text\"><configuration-text>%s</configuration-text></load-configuration>"
)
const sampleConfig = `interfaces {
ge-0/0/0 {
mtu 1400;
}
}`
func Configure(host string, user string, password string) error {
// Open SSH connection
s, err := netconf.DialSSH(host, &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
return fmt.Errorf("failed DialSSH: %w", err)
}
defer s.Close()
preCmds := []netconf.RawMethod{
netconf.RawMethod(rpcDiscard),
// Lock
// If already locked other process, returned &netconf.RPCError{..., Tag:"lock-denied", ...}
netconf.MethodLock("candidate"),
}
configCmds := []netconf.RawMethod{
netconf.RawMethod(fmt.Sprintf(rpcConfigLoadText, sampleConfig)),
}
postCmds := []netconf.RawMethod{
// Compare config. Get candidate compare (show | compare)
netconf.RawMethod(fmt.Sprintf(rpcGetCandidateCompare, 0)),
// Commit
netconf.RawMethod(rpcCommit),
// Unlock
netconf.MethodUnlock("candidate"),
}
// preCmds, configCmds, postCmds
cmds := append(preCmds, configCmds...)
cmds = append(cmds, postCmds...)
for _, cmd := range cmds {
r, err := s.Exec(cmd)
if err != nil {
// If delete non-existing config tree, returned &netconf.RPCError{..., Tag:"data-missing", ...}
return fmt.Errorf("failed rpc command: %s: %w", cmd, err)
}
slog.Info(fmt.Sprintf("Request: %s\nReply: %+v", cmd, r))
}
return nil
}
func main() {
err := Configure("10.100.2.94", "qiita", "qiita-password")
if err != nil {
slog.Error(fmt.Sprintf("%s", err))
}
}
先のコードを main.go
として保存してビルドします。(出力は省略)
$ go mod init example.com/qiita/go-netconf-unit-test
$ go mod tidy
$ go build
フォルダ名が go-netconf-unit-test
なので、出力されるビルド結果も go-netconf-unit-test
になります。
最低限の動作確認
設定前の状態確認
まずは元々のインターフェース設定を確認します。
ge-0/0/0に特に設定は入っておらず、MTUは Link-level type: Ethernet, MTU: 1514
ですね。
qiita@vjunos-03> show configuration interfaces
fxp0 {
unit 0 {
family inet {
address 10.100.2.94/22;
}
}
}
qiita@vjunos-03> show interfaces ge-0/0/0
Physical interface: ge-0/0/0, Enabled, Physical link is Up
Interface index: 141, SNMP ifIndex: 519
Link-level type: Ethernet, MTU: 1514, LAN-PHY mode, Speed: 1000mbps, BPDU Error: None, Loop Detect PDU Error: None,
Ethernet-Switching Error: None, MAC-REWRITE Error: None, Loopback: Disabled, Source filtering: Disabled, Flow control: Enabled,
Auto-negotiation: Enabled, Remote fault: Online, Media type: Fiber
Device flags : Present Running
Interface Specific flags: Internal: 0x101200
Interface flags: SNMP-Traps Internal: 0x4000
Link flags : None
CoS queues : 8 supported, 8 maximum usable queues
Current address: 16:29:3e:a6:56:04, Hardware address: 16:29:3e:a6:56:04
Last flapped : 2024-06-06 06:20:12 UTC (31w1d 03:21 ago)
Input rate : 0 bps (0 pps)
Output rate : 664 bps (0 pps)
Active alarms : None
Active defects : None
PCS statistics Seconds
Bit errors 0
Errored blocks 0
Ethernet FEC statistics Errors
FEC Corrected Errors 0
FEC Uncorrected Errors 0
FEC Corrected Errors Rate 0
FEC Uncorrected Errors Rate 0
Interface transmit statistics: Disabled
Logical interface ge-0/0/0.16386 (Index 346) (SNMP ifIndex 529)
Flags: Up SNMP-Traps 0x4004000 Encapsulation: ENET2
Input packets : 0
Output packets: 2
実行
では先ほどビルドした go-netconf-unit-test
を実行してみましょう。
リクエストとレスポンスのXMLデータをログに出力しているため、沢山の出力が確認されます。
人の目で見るようなものではありませんが、configuration-textで入力したい設定、configuration-outputで期待する差分(入力した設定が入っている、入力していない設定が入っていないこと)が確認できます。
$ ./go-netconf-unit-test
2025/01/31 17:54:23 INFO Request: <discard-changes/>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="0175ff5c-2688-4c2d-86e9-d68bd87ea856" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:0175ff5c-2688-4c2d-86e9-d68bd87ea856}
2025/01/31 17:54:23 INFO Request: <lock><target><candidate/></target></lock>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="936054e0-8363-436b-97da-29dc40b14dbf" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:936054e0-8363-436b-97da-29dc40b14dbf}
2025/01/31 17:54:23 INFO Request: <load-configuration action="merge" format="text"><configuration-text>interfaces {
ge-0/0/0 {
mtu 1400;
}
}</configuration-text></load-configuration>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<load-configuration-results>
<ok/>
</load-configuration-results>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="8f12d07f-fb0e-4103-aaf2-2394d307b1b5" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<load-configuration-results>
<ok/>
</load-configuration-results>
</rpc-reply>
MessageID:8f12d07f-fb0e-4103-aaf2-2394d307b1b5}
2025/01/31 17:54:23 INFO Request: <get-configuration compare="rollback" rollback="0" format="text"/>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<configuration-information>
<configuration-output>
[edit interfaces]
+ ge-0/0/0 {
+ mtu 1400;
+ }
</configuration-output>
</configuration-information>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="c036dce7-1865-4df2-b7a1-92ec2c314f79" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<configuration-information>
<configuration-output>
[edit interfaces]
+ ge-0/0/0 {
+ mtu 1400;
+ }
</configuration-output>
</configuration-information>
</rpc-reply>
MessageID:c036dce7-1865-4df2-b7a1-92ec2c314f79}
2025/01/31 17:54:25 INFO Request: <commit/>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="068cd57f-871b-47d1-920a-8acf4b2d1b6b" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:068cd57f-871b-47d1-920a-8acf4b2d1b6b}
2025/01/31 17:54:25 INFO Request: <unlock><target><candidate/></target></unlock>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="18ce27ad-553d-46d1-b279-76b95a2adb6b" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:18ce27ad-553d-46d1-b279-76b95a2adb6b}
設定後の状態確認
設定後のインターフェース状態を確認します。
ge-0/0/0に設定が入り、MTUが Link-level type: Ethernet, MTU: 1400
になりました。
qiita@vjunos-03> show configuration interfaces
ge-0/0/0 {
mtu 1400;
}
fxp0 {
unit 0 {
family inet {
address 10.100.2.94/22;
}
}
}
qiita@vjunos-03> show interfaces ge-0/0/0
Physical interface: ge-0/0/0, Enabled, Physical link is Up
Interface index: 141, SNMP ifIndex: 519
Link-level type: Ethernet, MTU: 1400, LAN-PHY mode, Speed: 1000mbps, BPDU Error: None, Loop Detect PDU Error: None,
Ethernet-Switching Error: None, MAC-REWRITE Error: None, Loopback: Disabled, Source filtering: Disabled, Flow control: Enabled,
Auto-negotiation: Enabled, Remote fault: Online, Media type: Fiber
Device flags : Present Running
Interface Specific flags: Internal: 0x101200
Interface flags: SNMP-Traps Internal: 0x4000
Link flags : None
CoS queues : 8 supported, 8 maximum usable queues
Current address: 16:29:3e:a6:56:04, Hardware address: 16:29:3e:a6:56:04
Last flapped : 2024-06-06 06:20:12 UTC (31w1d 03:26 ago)
Input rate : 0 bps (0 pps)
Output rate : 0 bps (0 pps)
Active alarms : None
Active defects : None
PCS statistics Seconds
Bit errors 0
Errored blocks 0
Ethernet FEC statistics Errors
FEC Corrected Errors 0
FEC Uncorrected Errors 0
FEC Corrected Errors Rate 0
FEC Uncorrected Errors Rate 0
Interface transmit statistics: Disabled
これで、状況に合わせて投入したい設定を変更するだけで、色々な設定に応用出来そうなことが確認できました。
ロックされていて設定に入れないケースの確認
それでは、ついでにエラーになるケースも見てみましょう。
例えば、誰かがメンテナンス作業中で設定モードをロックしている場合です。
qiita@vjunos-03> configure exclusive
warning: uncommitted changes will be discarded on exit
Entering configuration mode
この時にプログラムが動作すると、以下のようなエラーが確認できます。
$ ./go-netconf-unit-test
2025/01/31 18:03:08 ERROR failed rpc command: <discard-changes/>: netconf rpc [error] '
configuration database locked by:
esi terminal pts/0 (pid 18946) on since 2025-01-31 09:03:43 UTC
exclusive [edit]
'
メッセージは非常に分かりやすいですね。
configuration database locked
なので設定モードに入る前の <discard-changes/>
で失敗していることが分かります。
しかし、出力がXMLではないせいか、この例ではあまり詳細な情報が分かりません。
間違った設定を入れようとしたケースの確認
では、投入用の設定が間違っている場合はどうでしょうか?
例えば、以下のようにMTUサイズを範囲外に指定してしまったとします。
@@ -17,7 +17,7 @@
const sampleConfig = `interfaces {
ge-0/0/0 {
- mtu 1;
+ mtu 1400;
}
}`
Goのプログラムとしては特に間違っていませんから、コンパイルは通ってしまいます。
再度実行してみると、何やら処理は行われているようですが、先ほどは無かった <rpc-error>
が出力されています。
しかも、そのままcommitまでされてしまいました!
$ ./go-netconf-unit-test
2025/01/31 18:51:21 INFO Request: <discard-changes/>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="f7a77ee6-c47f-424a-aa1a-0c80bcf9e102" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:f7a77ee6-c47f-424a-aa1a-0c80bcf9e102}
2025/01/31 18:51:21 INFO Request: <lock><target><candidate/></target></lock>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="000df840-d019-453c-b64e-76abf1da5af2" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:000df840-d019-453c-b64e-76abf1da5af2}
2025/01/31 18:51:21 INFO Request: <load-configuration action="merge" format="text"><configuration-text>interfaces {
ge-0/0/0 {
mtu 1;
}
}</configuration-text></load-configuration>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<load-configuration-results>
<rpc-error>
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<error-message>Value 1 is not within range (256..9192)</error-message>
<error-info>
<bad-element>1</bad-element>
</error-info>
</rpc-error>
<rpc-error>
<error-severity>warning</error-severity>
<error-path>[edit interfaces]</error-path>
<error-message>mgd: statement has no contents; ignored</error-message>
<error-info>
<bad-element>ge-0/0/0</bad-element>
</error-info>
</rpc-error>
</load-configuration-results>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="0629d92f-78b9-48fd-99d6-42a7027bc853" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<load-configuration-results>
<rpc-error>
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<error-message>Value 1 is not within range (256..9192)</error-message>
<error-info>
<bad-element>1</bad-element>
</error-info>
</rpc-error>
<rpc-error>
<error-severity>warning</error-severity>
<error-path>[edit interfaces]</error-path>
<error-message>mgd: statement has no contents; ignored</error-message>
<error-info>
<bad-element>ge-0/0/0</bad-element>
</error-info>
</rpc-error>
</load-configuration-results>
</rpc-reply>
MessageID:0629d92f-78b9-48fd-99d6-42a7027bc853}
2025/01/31 18:51:21 INFO Request: <get-configuration compare="rollback" rollback="0" format="text"/>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<configuration-information>
<configuration-output>
</configuration-output>
</configuration-information>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="f2a9f478-5d6b-481b-b688-d213fd9a7448" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<configuration-information>
<configuration-output>
</configuration-output>
</configuration-information>
</rpc-reply>
MessageID:f2a9f478-5d6b-481b-b688-d213fd9a7448}
2025/01/31 18:51:21 INFO Request: <commit/>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="51082295-a30f-429f-a989-74ae8e96a17f" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:51082295-a30f-429f-a989-74ae8e96a17f}
2025/01/31 18:51:21 INFO Request: <unlock><target><candidate/></target></unlock>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="3c86e33c-0dad-42cf-b78d-624645023f2a" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:3c86e33c-0dad-42cf-b78d-624645023f2a}
これではいけません。どのようなエラーが起きているのでしょうか?
該当部分を抜き出して整形してみます。
<load-configuration-results>
<rpc-error>
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<error-message>Value 1 is not within range (256..9192)</error-message>
<error-info>
<bad-element>1</bad-element>
</error-info>
</rpc-error>
<rpc-error>
<error-severity>warning</error-severity>
<error-path>[edit interfaces]</error-path>
<error-message>mgd: statement has no contents; ignored</error-message>
<error-info>
<bad-element>ge-0/0/0</bad-element>
</error-info>
</rpc-error>
</load-configuration-results>
エラーを表しているのは <error-severity>error</error-severity>
の登録のようですね。
対応している rpc-error
にどのようなものがあるのか見てみましょう。
これには RFC 6241: Network Configuration Protocol (NETCONF) が参考になりますね!
79ページ目に <error-tag>operation-failed</error-tag>
で示されているタグの説明を見つけることができました。
RFC 6241 NETCONF Protocol June 2011
error-tag: operation-failed
error-type: rpc, protocol, application
error-severity: error
error-info: none
Description: Request could not be completed because the requested
operation failed for some reason not covered by
any other error condition.
何らかの理由でリクエストしたオペレーションが失敗した時に使用するものですね。
vJunos-Switchの場合は投入不可能な設定をしようとした場合は operation-failed
が使われるようです。
しかし、エラーの内容は分かりましたが、このままcommit処理まで進んでしまうのは良くありません。
修正方法を考えてみましょう。
エラーハンドリングを修正してみる
github.com/Juniper/go-netconf/netconf
には RPCError
の型定義がありました。
https://github.com/Juniper/go-netconf/blob/master/netconf/rpc.go#L94-L101
これを利用してエラータグが返ってきたときに処理を中断できるようにXMLの解析コードを追加してみます。
diff --git a/main.go b/main.go
index e088ed2..51c6dd3 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "encoding/xml"
"fmt"
"log/slog"
@@ -17,10 +18,14 @@ const (
const sampleConfig = `interfaces {
ge-0/0/0 {
- mtu 1400;
+ mtu 1;
}
}`
+type LoadConfigurationResults struct {
+ Errors []netconf.RPCError `xml:"rpc-error"`
+}
+
func Configure(host string, user string, password string) error {
// Open SSH connection
s, err := netconf.DialSSH(host, &ssh.ClientConfig{
@@ -59,6 +64,13 @@ func Configure(host string, user string, password string) error {
// If delete non-existing config tree, returned &netconf.RPCError{..., Tag:"data-missing", ...}
return fmt.Errorf("failed rpc command: %s: %w", cmd, err)
}
+ var results LoadConfigurationResults
+ if err := xml.Unmarshal([]byte(r.Data), &results); err != nil {
+ return fmt.Errorf("failed parse load-configuration-results: %s: %w", cmd, err)
+ }
+ if len(results.Errors) > 0 {
+ return fmt.Errorf("failed load-configuration: %s: %+v", cmd, results.Errors)
+ }
slog.Info(fmt.Sprintf("Request: %s\nReply: %+v", cmd, r))
}
return nil
エラーハンドリングを修正後の出力例
差分を適用してビルドしたら、再度MTU=1の設定を入れようとしてNW装置に怒られてみましょう。
$ ./go-netconf-unit-test
2025/01/31 19:05:34 INFO Request: <discard-changes/>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="141808e9-20b2-4652-8448-aed9d19022be" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:141808e9-20b2-4652-8448-aed9d19022be}
2025/01/31 19:05:34 INFO Request: <lock><target><candidate/></target></lock>
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<ok/>
Ok:false RawReply:<rpc-reply xmlns:junos="http://xml.juniper.net/junos/23.1R0/junos" message-id="ecc4cfc0-ab27-4cbc-92e0-2fb314fb93ad" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<ok/>
</rpc-reply>
MessageID:ecc4cfc0-ab27-4cbc-92e0-2fb314fb93ad}
2025/01/31 19:05:34 ERROR failed load-configuration: <load-configuration action="merge" format="text"><configuration-text>interfaces {
ge-0/0/0 {
mtu 1;
}
}</configuration-text></load-configuration>: [{Type:protocol Tag:operation-failed Severity:error Path: Message:Value 1 is not within range (256..9192) Info:
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<error-message>Value 1 is not within range (256..9192)</error-message>
<error-info>
<bad-element>1</bad-element>
</error-info>
} {Type: Tag: Severity:warning Path:[edit interfaces] Message:mgd: statement has no contents; ignored Info:
<error-severity>warning</error-severity>
<error-path>[edit interfaces]</error-path>
<error-message>mgd: statement has no contents; ignored</error-message>
<error-info>
<bad-element>ge-0/0/0</bad-element>
</error-info>
}]
今度はちゃんとエラーが返ってきたタイミングで処理が中断されて、エラーメッセージが表示されるようになりました。
これなら範囲外のパラメータが入ってしまっても、ひとまず安心ですね。
まとめ
今回は github.com/Juniper/go-netconf/netconf
を利用して、Go言語を利用したNW装置とのNETCONF通信をしてみました。
NW装置に設定する内容は要件に応じて様々ですが、今回の内容をもとに設定内容や変数化するパラメータをテンプレート化することで、ごく基本的な機能を持ったNWサービスの開発が可能になると思います。
また、ある程度RFCで規範が与えられているとはいえ、エラー発生時の応答はNW装置によって実装が様々です。
表面的なコード理解だけでなく、生データを観測しながら正しいらしい動作を作っていくことが重要ですね。