Skip to content

Commit 77eecde

Browse files
committed
Add basic tests
1 parent bdedc2e commit 77eecde

14 files changed

Lines changed: 571 additions & 81 deletions

LICENSE.txt

Lines changed: 373 additions & 0 deletions
Large diffs are not rendered by default.

Makefile

Lines changed: 0 additions & 5 deletions
This file was deleted.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
# go-dnp3
22

3+
[![GoDoc](https://godoc.org/github.com/nblair2/go-dnp3?status.svg)](https://godoc.org/github.com/nblair2/go-dnp3)
4+
![Go Version](https://img.shields.io/github/go-mod/go-version/nblair2/go-dnp3?filename=go.mod&style=flat-square)
5+
![License](https://img.shields.io/github/license/nblair2/go-dnp3?style=flat-square)
6+
7+
38
DNP3 parsing in go.
49
* **`FromBytes([]byte)`** to parse a byte slice and interpret it as DNP3
510
* **`ToBytes()`** to go from a struct back to bytes (calculates length, calculates and inserts CRCs on the way)
611
* **`String()`** to get packet as human-readable indented string (Reserved and CRCs not shown)
7-
* **`json.MarshalIndent(dnp, "", " ")`** to get packet as machine-friendly
12+
* **`json.Marshal(DNP3)`** to get packet as machine-friendly
813

914
## Improvements
1015

@@ -18,7 +23,7 @@ DNP3 parsing in go.
1823

1924
## Test
2025

21-
Run `make test` to check `examples/*.pcap` for errors. Data taken from [opendnp3 conformance reports](https://dnp3.github.io/conformance/report.html) and [ITI ICS Security Tools Repository](https://github.com/ITI/ICS-Security-Tools/tree/master/pcaps/dnp3)
26+
Run `go test -v` to check a few different DNP3 messages. You can also use the `-args -pcaps=examples/opendnp3_test1.pcap` argument to pass in a full PCAP. Data taken from [opendnp3 conformance reports](https://dnp3.github.io/conformance/report.html).
2227

2328
## Spec
2429

dnp3/dnp3.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
// partial implementation of DNP3 for gopacket
1+
// DNP3 (Distributed Network Protocol version 3) is a SCADA protocol used in
2+
// industrial automation, especially electric power and water services in
3+
// North America. See dnp.org, IEEE-1815.
4+
// The protocol consists of three layers: A data link layer, a transport layer,
5+
// and an application layer.
26
package dnp3
37

48
import (
@@ -7,11 +11,6 @@ import (
711
"github.com/google/gopacket"
812
)
913

10-
// DNP3 (Distributed Network Protocol version 3) is a SCADA protocol used in
11-
// industrial automation, especially electric power and water services in
12-
// North America. See dnp.org, IEEE-1815.
13-
// The protocol consists of three layers: A data link layer, a transport layer,
14-
// and an application layer.
1514
type DNP3 struct {
1615
DataLink DataLink
1716
Transport Transport

dnp3/dnp3_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package dnp3
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"slices"
8+
"strings"
9+
"testing"
10+
11+
"github.com/google/gopacket"
12+
"github.com/google/gopacket/layers"
13+
"github.com/google/gopacket/pcap"
14+
)
15+
16+
func testFromBytesToBytesStringMarshal(t *testing.T, input []byte) {
17+
d := DNP3{}
18+
err := d.FromBytes(input)
19+
if err != nil {
20+
t.Fatal("FromBytes:", err)
21+
}
22+
23+
output, err := d.ToBytes()
24+
if err != nil {
25+
t.Fatal("ToBytes:", err)
26+
}
27+
28+
if !slices.Equal(input, output) {
29+
t.Fatal("Input and Output not equal")
30+
}
31+
32+
_ = d.String()
33+
34+
_, err = json.MarshalIndent(d, "", " ")
35+
if err != nil {
36+
t.Fatal("MarshalIndent:", err)
37+
}
38+
}
39+
40+
func TestDNP3(t *testing.T) {
41+
tests := []struct {
42+
name string
43+
input []byte
44+
}{
45+
{
46+
"Request/ReadClass1230",
47+
[]byte{
48+
0x05, 0x64, 0x14, 0xc4, 0x04, 0x00, 0x03, 0x00,
49+
0xc7, 0x17, 0xc4, 0xc5, 0x01, 0x3c, 0x02, 0x06,
50+
0x3c, 0x03, 0x06, 0x3c, 0x04, 0x06, 0x3c, 0x01,
51+
0x06, 0xa3, 0x61,
52+
},
53+
},
54+
{
55+
"Request/ReadBinaryInputChange",
56+
[]byte{
57+
0x05, 0x64, 0x0b, 0xc4, 0x00, 0x04, 0x01, 0x00,
58+
0xca, 0x8a, 0xc0, 0xc1, 0x01, 0x02, 0x00, 0x06,
59+
0x95, 0x76,
60+
},
61+
},
62+
{
63+
"Request/WriteTime",
64+
[]byte{
65+
0x05, 0x64, 0x12, 0xc4, 0x04, 0x00, 0x03, 0x00,
66+
0x1e, 0x7c, 0xc1, 0xc1, 0x02, 0x32, 0x01, 0x07,
67+
0x01, 0xeb, 0xe4, 0x5a, 0x87, 0xff, 0x00, 0x28,
68+
0x01,
69+
},
70+
},
71+
{
72+
"Request/Select",
73+
[]byte{
74+
0x05, 0x64, 0x1a, 0xc4, 0x04, 0x00, 0x03, 0x00,
75+
0xc2, 0xe6, 0xd2, 0xc3, 0x03, 0x0c, 0x01, 0x28,
76+
0x01, 0x00, 0x9f, 0x86, 0x03, 0x01, 0x64, 0x00,
77+
0x00, 0x00, 0xec, 0x41, 0x64, 0x00, 0x00, 0x00,
78+
0x00, 0x00, 0x5b,
79+
},
80+
},
81+
{
82+
"Response/AllIINSet",
83+
[]byte{
84+
0x05, 0x64, 0x0a, 0x44, 0x03, 0x00, 0x04, 0x00,
85+
0x7c, 0xae, 0xe7, 0xc1, 0x81, 0xff, 0x3f, 0x1c,
86+
0x48,
87+
},
88+
},
89+
{
90+
"Response/GV_02-02",
91+
[]byte{
92+
0x05, 0x64, 0x2a, 0x44, 0x01, 0x00, 0x00, 0x04,
93+
0xe5, 0x79, 0xc1, 0xe2, 0x81, 0x90, 0x00, 0x02,
94+
0x02, 0x28, 0x03, 0x00, 0x00, 0x00, 0x81, 0xda,
95+
0x33, 0xd2, 0xdf, 0xe5, 0x64, 0x71, 0x01, 0x00,
96+
0x00, 0x01, 0xda, 0x33, 0xd2, 0x64, 0x71, 0x01,
97+
0xff, 0xff, 0x81, 0xdb, 0xdd, 0x14, 0x33, 0xd2,
98+
0x64, 0x71, 0x01, 0x38, 0x5d,
99+
},
100+
},
101+
{
102+
"Response/GV_01-01_10-02_20-05_21-09_30-03",
103+
[]byte{
104+
0x05, 0x64, 0x4e, 0x44, 0x03, 0x00, 0x04, 0x00,
105+
0x6f, 0x4d, 0xc7, 0xc7, 0x81, 0x00, 0x00, 0x01,
106+
0x01, 0x00, 0x00, 0x05, 0x19, 0x0a, 0x02, 0x00,
107+
0x00, 0x05, 0xc3, 0x47, 0x81, 0x01, 0x81, 0x81,
108+
0x01, 0x01, 0x14, 0x05, 0x00, 0x00, 0x00, 0x20,
109+
0x00, 0x00, 0x00, 0x15, 0xf1, 0x7b, 0x09, 0x00,
110+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x03,
111+
0x00, 0x00, 0x06, 0xca, 0x00, 0x00, 0x18, 0x7e,
112+
0x00, 0xcb, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00,
113+
0x00, 0xff, 0xff, 0xff, 0xff, 0x66, 0x21, 0x00,
114+
0xd6, 0xf3, 0x00, 0x59, 0x21, 0x00, 0x00, 0x4b,
115+
0x21, 0x00, 0x00, 0xe0, 0x51,
116+
},
117+
},
118+
}
119+
120+
for _, tc := range tests {
121+
t.Run(tc.name, func(t *testing.T) {
122+
testFromBytesToBytesStringMarshal(t, tc.input)
123+
})
124+
}
125+
}
126+
127+
var custPcaps []string
128+
129+
func init() {
130+
flag.Func("pcaps", "Comma-separated list of pcap files to read", func(val string) error {
131+
if val != "" {
132+
custPcaps = append(custPcaps, splitComma(val)...)
133+
}
134+
return nil
135+
})
136+
}
137+
138+
func splitComma(s string) []string {
139+
var out []string
140+
for _, v := range strings.Split(s, ",") {
141+
v = strings.TrimSpace(v)
142+
if v != "" {
143+
out = append(out, v)
144+
}
145+
}
146+
return out
147+
}
148+
149+
func TestCustomPcaps(t *testing.T) {
150+
flag.Parse()
151+
if len(custPcaps) == 0 {
152+
t.Skip("No custom pcap file provided")
153+
}
154+
155+
for _, pcapFile := range custPcaps {
156+
t.Run(pcapFile, func(t *testing.T) {
157+
158+
handle, err := pcap.OpenOffline(pcapFile)
159+
if err != nil {
160+
t.Skipf("Error opening PCAP: %v", err)
161+
}
162+
defer handle.Close()
163+
164+
pcap := gopacket.NewPacketSource(handle, handle.LinkType())
165+
i := 0
166+
for pkt := range pcap.Packets() {
167+
168+
i += 1
169+
tcpLayer := pkt.Layer(layers.LayerTypeTCP)
170+
if tcpLayer != nil {
171+
172+
tcp, _ := tcpLayer.(*layers.TCP)
173+
input := tcp.Payload
174+
if len(input) < 10 {
175+
continue
176+
}
177+
178+
t.Run(fmt.Sprintf("Packet%d", i), func(t *testing.T) {
179+
testFromBytesToBytesStringMarshal(t, input)
180+
})
181+
}
182+
}
183+
})
184+
}
185+
}

dnp3/opendnp3_test1.pcap

399 Bytes
Binary file not shown.

examples/iti_testp1.pcap

-15.5 KB
Binary file not shown.

examples/iti_testp2.pcap

-2.91 KB
Binary file not shown.

examples/opendnp3_test1.pcap

-1.43 KB
Binary file not shown.

examples/opendnp3_test2.pcap

-1.43 KB
Binary file not shown.

0 commit comments

Comments
 (0)