aboutsummaryrefslogtreecommitdiff
path: root/six
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-02-13 06:02:52 +0000
committerMike Crute <mike@crute.us>2020-02-13 06:21:45 +0000
commit651151bce6be01a5cef0e8eb400e0a028b93d887 (patch)
treebc4dcf69fea04502df198117395a804db60595d8 /six
downloadsix_monitoring-651151bce6be01a5cef0e8eb400e0a028b93d887.tar.bz2
six_monitoring-651151bce6be01a5cef0e8eb400e0a028b93d887.tar.xz
six_monitoring-651151bce6be01a5cef0e8eb400e0a028b93d887.zip
Initial importHEADmaster
Diffstat (limited to 'six')
-rw-r--r--six/feed_parser.go159
-rw-r--r--six/helpers.go112
-rw-r--r--six/info_file_parser.go279
-rw-r--r--six/info_file_parser_test.go256
-rw-r--r--six/participant.go224
-rw-r--r--six/participant_builder.go113
6 files changed, 1143 insertions, 0 deletions
diff --git a/six/feed_parser.go b/six/feed_parser.go
new file mode 100644
index 0000000..fb839f7
--- /dev/null
+++ b/six/feed_parser.go
@@ -0,0 +1,159 @@
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (C) 2020 Michael Crute <mike@crute.us>. All rights reserved.
3//
4// Use of this source code is governed by a license that can be found in the
5// LICENSE file.
6
7package six
8
9import (
10 "encoding/csv"
11 "fmt"
12 "io"
13 "net/http"
14 "os"
15)
16
17const sixFeedUrl = "https://www.seattleix.net/autogen/participants.csv"
18
19// Error indicating that a line in the SIX CSV feed could not be parsed.
20// Contains the actual error as well as the line number on which the error
21// occurred.
22type SIXParticipantParseError struct {
23 Line int
24 Err error
25}
26
27// Return the error string for the error
28func (e *SIXParticipantParseError) Error() string {
29 return fmt.Sprintf("record on line %d: %s", e.Line, e.Err)
30}
31
32// Returns the wrapped error
33func (e *SIXParticipantParseError) Unwrap() error {
34 return e.Err
35}
36
37// Parser for the SIX participant CSV feed.
38type SIXParser struct {
39 Records []*SIXParticipant
40 Errors []error
41 asnIndex map[int][]*SIXParticipant
42 headers []string
43}
44
45// Builds a new SIXParser
46func NewSIXParser() *SIXParser {
47 return &SIXParser{
48 Records: []*SIXParticipant{},
49 Errors: []error{},
50 asnIndex: map[int][]*SIXParticipant{},
51 headers: nil,
52 }
53}
54
55// After parsing the feed will return a list of participant structs for the
56// passed ASN as well as a boolean indicating if the participant exists.
57// Participants may have several connections to the exchange and each
58// connection is a different participant.
59func (p *SIXParser) GetParticipantByASN(asn int) ([]*SIXParticipant, bool) {
60 r, ok := p.asnIndex[asn]
61 return r, ok
62}
63
64// Adds a participant to the SIXParticipant struct
65func (p *SIXParser) AddParticipant(sp *SIXParticipant) {
66 p.Records = append(p.Records, sp)
67
68 if _, ok := p.asnIndex[sp.ASN]; !ok {
69 p.asnIndex[sp.ASN] = []*SIXParticipant{sp}
70 } else {
71 p.asnIndex[sp.ASN] = append(p.asnIndex[sp.ASN], sp)
72 }
73}
74
75func (p *SIXParser) sliceToHeaderMap(s []string) map[string]string {
76 r := map[string]string{}
77 for i, k := range p.headers {
78 r[k] = s[i]
79 }
80 return r
81}
82
83func (p *SIXParser) addRow(rn int, r []string) {
84 defer func() {
85 if e := recover(); e != nil {
86 p.Errors = append(p.Errors, &SIXParticipantParseError{
87 Line: rn,
88 Err: e.(error),
89 })
90 }
91 }()
92 p.AddParticipant(NewSIXParticipantFromData(p.sliceToHeaderMap(r)))
93}
94
95// ASSUMPTION: The correct row size will not change
96//
97// Participants that aren't using the route servers tend to have inconsistent
98// row data. The head and tail of the row are fine and contain the organization
99// and connection information but the middle area that contains route server
100// stats generally has 7-9 too few columns. This function will graft the head
101// and tail onto an appropriately sized row.
102func (p *SIXParser) fixupRow(r []string) []string {
103 k := make([]string, 49)
104 copy(k[:18], r[:18])
105 copy(k[42:], r[len(r)-7:])
106 return k
107}
108
109// Parse an io.Reader containing SIX participant data in CSV format and collect
110// the results into the parser for later querying.
111func (p *SIXParser) Parse(fr io.Reader) {
112 rn := 0
113 cr := csv.NewReader(fr)
114 for {
115 rn++
116 row, err := cr.Read()
117 if err == io.EOF {
118 return
119 }
120 if err != nil {
121 row = p.fixupRow(row)
122 p.Errors = append(p.Errors, err)
123 }
124 if p.headers == nil {
125 p.headers = row
126 continue
127 }
128 p.addRow(rn, row)
129 }
130}
131
132// Create a new SIXParser and parse the CSV file pointed to by the filename.
133func ParseSIXCSV(filename string) (*SIXParser, error) {
134 fp, err := os.Open(filename)
135 if err != nil {
136 return nil, err
137 }
138 defer fp.Close()
139
140 sp := NewSIXParser()
141 sp.Parse(fp)
142
143 return sp, nil
144}
145
146// Create a new SIXParser and parse the contents of the SIX participant file
147// locate on the SIX http server.
148func FetchParseSIXCSV() (*SIXParser, error) {
149 res, err := http.Get(sixFeedUrl)
150 if err != nil {
151 return nil, err
152 }
153 defer res.Body.Close()
154
155 sp := NewSIXParser()
156 sp.Parse(res.Body)
157
158 return sp, nil
159}
diff --git a/six/helpers.go b/six/helpers.go
new file mode 100644
index 0000000..694a79e
--- /dev/null
+++ b/six/helpers.go
@@ -0,0 +1,112 @@
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (C) 2020 Michael Crute <mike@crute.us>. All rights reserved.
3//
4// Use of this source code is governed by a license that can be found in the
5// LICENSE file.
6
7package six
8
9import (
10 "net"
11 "strconv"
12 "strings"
13 "time"
14)
15
16func mustParseInt(a string) int {
17 a = strings.TrimSpace(a)
18
19 if a == "" {
20 return 0
21 }
22
23 i, err := strconv.Atoi(a)
24 if err != nil {
25 panic(err)
26 }
27
28 return i
29}
30
31func parseYesNo(b string) bool {
32 return strings.ToLower(strings.TrimSpace(b)) == "yes"
33}
34
35func parseIP(i string) *net.IP {
36 o := net.ParseIP(i)
37 return &o
38}
39
40func parseIPNetFromCIDR(i string) *net.IPNet {
41 _, ipnet, _ := net.ParseCIDR(strings.TrimSpace(i))
42 return ipnet
43}
44
45func mustParseTime(t string) *time.Time {
46 t = strings.TrimSpace(t)
47
48 if t == "" {
49 return nil
50 }
51
52 i, err := time.Parse("2006-01-02", t)
53 if err != nil {
54 panic(err)
55 }
56
57 return &i
58}
59
60func mustParseLongTime(t string) *time.Time {
61 t = strings.TrimSpace(t)
62
63 if t == "" {
64 return nil
65 }
66
67 i, err := time.Parse("2006-01-02 15:04:05 MST", t)
68 if err != nil {
69 panic(err)
70 }
71
72 return &i
73}
74
75func mustParseLongTimeNoZone(t string) *time.Time {
76 t = strings.TrimSpace(t)
77
78 if t == "" {
79 return nil
80 }
81
82 i, err := time.Parse("2006-01-02 15:04:05", t)
83 if err != nil {
84 panic(err)
85 }
86
87 return &i
88}
89
90func allEmpty(d []string) bool {
91 for _, v := range d {
92 if strings.TrimSpace(v) != "" {
93 return false
94 }
95 }
96 return true
97}
98
99func parseASPath(p string) []int {
100 out := []int{}
101
102 for _, i := range strings.Split(p, " ") {
103 ii, err := strconv.Atoi(strings.TrimSpace(i))
104 // Some AS paths contain { and } which are not valid and need to be discard
105 if err != nil {
106 continue
107 }
108 out = append(out, ii)
109 }
110
111 return out
112}
diff --git a/six/info_file_parser.go b/six/info_file_parser.go
new file mode 100644
index 0000000..cf98280
--- /dev/null
+++ b/six/info_file_parser.go
@@ -0,0 +1,279 @@
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (C) 2020 Michael Crute <mike@crute.us>. All rights reserved.
3//
4// Use of this source code is governed by a license that can be found in the
5// LICENSE file.
6
7package six
8
9import (
10 "fmt"
11 "io/ioutil"
12 "net"
13 "net/http"
14 "regexp"
15 "strings"
16)
17
18const (
19 roaUrl = "https://www.seattleix.net/rs/rpki_roas/%d.txt" // ASN
20 pfxUrl = "https://www.seattleix.net/rs/irr_prefixes/%d.v%d.txt" // ASN, IP Version
21 asnUrl = "https://www.seattleix.net/rs/irr_asns/%d.v%d.txt" // ASN, IP Version
22 asSetUrl = "https://www.seattleix.net/rs/irr_as-set_prefixes/%d.v%d.txt" // ASN, IP Version
23 rsPrefixUrl = "https://www.seattleix.net/rs/rs%d.%d.v%d/%d:1:%d:v%d.txt" // RS#, MTU, IP Version, ASN, MTU, IP Version
24 rsErrUrl = "https://www.seattleix.net/rs/rs%d.%d.v%d/%d:1:%d:v%d.%serr.txt" // RS#, MTU, IP Version, ASN, MTU, IP Version, x for xerr
25)
26
27// Date, ASN, MTU, IP Version, Net Name, Network, Source, AS Path (may have {}), Error Message
28// See tests for line examples
29var errorLineRegex = regexp.MustCompile(`(?P<datetime>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) <[^>]+> (?P<asn>\d+):1:(?P<mtu>\d+):v(?P<ip_version>[46]):(?P<net_name>[^:]+): (?P<target>(?:[0-9\.\/]+|[0-9a-f\/:]+)) from (?P<source>(?:[0-9\.]+|[0-9a-f:]+?)):? (?:(?:bgp_path)?\(path (?P<path>[\d {}]+)\):?)? ?(?P<error>.*)`)
30
31func fetchParseRSErrors(rsNum, asn int, forTransit bool) *Errors {
32 transit := ""
33 if forTransit {
34 transit = "x"
35 }
36
37 // Not everyone will have every file and parseErrorFiles ignores nil so
38 // just ignore errors here
39 v4, _ := fetchFile(rsErrUrl, rsNum, 1500, IPv4, asn, 1500, IPv4, transit)
40 v6, _ := fetchFile(rsErrUrl, rsNum, 1500, IPv6, asn, 1500, IPv6, transit)
41 v4j, _ := fetchFile(rsErrUrl, rsNum, 9000, IPv4, asn, 9000, IPv4, transit)
42 v6j, _ := fetchFile(rsErrUrl, rsNum, 9000, IPv6, asn, 9000, IPv6, transit)
43
44 return parseErrorFiles(v4, v6, v4j, v6j)
45}
46
47func fetchParseRoutes(rsNum, asn int, ipv IPVersion, forJumbo bool) (*RouteFile, error) {
48 mtu := 1500
49 if forJumbo {
50 mtu = 9000
51 }
52
53 f, err := fetchFile(rsPrefixUrl, rsNum, mtu, ipv, asn, mtu, ipv)
54 if err != nil {
55 return nil, err
56 }
57
58 return parseRoutesFile(f), nil
59}
60
61func fetchParseASSet(asn int, ipv IPVersion) (*ASSet, error) {
62 f, err := fetchFile(asnUrl, asn, ipv)
63 if err != nil {
64 return nil, err
65 }
66 return parseASSetFile(f), nil
67}
68
69func fetchParsePrefixList(asn int, ipv IPVersion, forAsSet bool) (*PrefixSet, error) {
70 url := pfxUrl
71 if forAsSet {
72 url = asSetUrl
73 }
74
75 f, err := fetchFile(url, asn, ipv)
76 if err != nil {
77 return nil, err
78 }
79 return parsePrefixSetFile(f), nil
80}
81
82func fetchParseROAFile(asn int) (*ROASet, error) {
83 f, err := fetchFile(roaUrl, asn)
84 if err != nil {
85 return nil, err
86 }
87 return parseROAFile(f), nil
88}
89
90func fetchFile(urlTpl string, args ...interface{}) ([]string, error) {
91 r, err := http.Get(fmt.Sprintf(urlTpl, args...))
92 if err != nil {
93 return nil, err
94 }
95 defer r.Body.Close()
96
97 b, err := ioutil.ReadAll(r.Body)
98 if err != nil {
99 return nil, err
100 }
101
102 return strings.Split(string(b), "\n"), nil
103}
104
105func parseROAFile(lines []string) *ROASet {
106 rs := &ROASet{
107 ROAS: []ROA{},
108 }
109
110 for _, line := range lines {
111 if strings.HasPrefix(line, "Timestamp:") {
112 rs.Timestamp = *mustParseLongTime(line[11:])
113 continue
114 } else if strings.HasPrefix(line, "Trust Anchor") {
115 continue
116 } else if strings.TrimSpace(line) == "" {
117 continue
118 } else {
119 lp := strings.Split(line, "|")
120 rs.ROAS = append(rs.ROAS, ROA{
121 TrustAnchor: strings.TrimSpace(lp[0]),
122 ASN: mustParseInt(lp[1]),
123 MaxLength: mustParseInt(lp[2]),
124 Prefix: *parseIPNetFromCIDR(lp[3]),
125 })
126 }
127 }
128
129 return rs
130}
131
132func parsePrefixSetFile(lines []string) *PrefixSet {
133 ps := &PrefixSet{
134 Prefixes: []net.IPNet{},
135 }
136
137 for _, line := range lines {
138 if strings.HasPrefix(line, "Timestamp:") {
139 ps.Timestamp = *mustParseLongTime(line[11:])
140 continue
141 } else if strings.HasPrefix(line, "as-set:") {
142 ps.ASSet = &strings.Split(line, " ")[1]
143 continue
144 } else if strings.TrimSpace(line) == "" {
145 continue
146 } else {
147 // Data formatting error on SIX side
148 if strings.HasSuffix(line, ",") {
149 line = line[:len(line)-1]
150 }
151 ps.Prefixes = append(ps.Prefixes, *parseIPNetFromCIDR(line))
152 }
153 }
154
155 return ps
156}
157
158func parseASSetFile(lines []string) *ASSet {
159 as := &ASSet{
160 ASNumbers: []int{},
161 }
162
163 for _, line := range lines {
164 if strings.HasPrefix(line, "Timestamp:") {
165 as.Timestamp = *mustParseLongTime(line[11:])
166 continue
167 } else if strings.HasPrefix(line, "as-set:") {
168 as.Name = strings.Split(line, " ")[1]
169 continue
170 } else if strings.TrimSpace(line) == "" {
171 continue
172 } else {
173 as.ASNumbers = append(as.ASNumbers, mustParseInt(line))
174 }
175 }
176
177 return as
178}
179
180func parseErrorFiles(v4, v6, v4j, v6j []string) *Errors {
181 e := &Errors{}
182
183 if v4 != nil {
184 parseErrorFile(v4, e, IPv4, false)
185 }
186 if v6 != nil {
187 parseErrorFile(v6, e, IPv6, false)
188 }
189 if v4j != nil {
190 parseErrorFile(v4j, e, IPv4, true)
191 }
192 if v6j != nil {
193 parseErrorFile(v6j, e, IPv6, true)
194 }
195
196 return e
197}
198
199func getTarget(e *Errors, ipv IPVersion, jumbo bool) *ErrorRecords {
200 if ipv == IPv4 && jumbo {
201 if e.IPv4Jumbo == nil {
202 e.IPv4Jumbo = NewErrorRecords()
203 }
204 return e.IPv4Jumbo
205 } else if ipv == IPv6 && jumbo {
206 if e.IPv6Jumbo == nil {
207 e.IPv6Jumbo = NewErrorRecords()
208 }
209 return e.IPv6Jumbo
210 } else if ipv == IPv4 {
211 if e.IPv4 == nil {
212 e.IPv4 = NewErrorRecords()
213 }
214 return e.IPv4
215 } else if ipv == IPv6 {
216 if e.IPv6 == nil {
217 e.IPv6 = NewErrorRecords()
218 }
219 return e.IPv6
220 } else {
221 // This should not be possible
222 panic("Could not find target")
223 }
224}
225
226func parseErrorFile(lines []string, e *Errors, ipv IPVersion, jumbo bool) {
227 t := getTarget(e, ipv, jumbo)
228
229 for _, line := range lines {
230 l := parseErrorLine(line)
231 if l == nil {
232 // Skip blank lines, which sometimes happen
233 if strings.TrimSpace(line) == "" {
234 continue
235 } else {
236 t.UnparsableLines = append(t.UnparsableLines, line)
237 }
238 } else {
239 t.Records = append(t.Records, *l)
240 }
241 }
242}
243
244func parseErrorLine(line string) *ErrorRecord {
245 res := errorLineRegex.FindAllStringSubmatch(line, -1)
246 if len(res) == 0 {
247 return nil
248 } else {
249 row := res[0][1:]
250 return &ErrorRecord{
251 Timestamp: *mustParseLongTimeNoZone(row[0]),
252 ASN: mustParseInt(row[1]),
253 MTU: mustParseInt(row[2]),
254 Version: IPVersion(mustParseInt(row[3])),
255 NetworkName: row[4],
256 Network: *parseIPNetFromCIDR(row[5]),
257 Router: *parseIP(row[6]),
258 Path: parseASPath(row[7]),
259 Message: row[8],
260 }
261 }
262}
263
264func parseRoutesFile(lines []string) *RouteFile {
265 ps := &RouteFile{
266 Lines: []string{},
267 }
268 for _, line := range lines {
269 if strings.HasPrefix(line, "Timestamp:") {
270 ps.Timestamp = *mustParseLongTime(line[11:])
271 continue
272 } else if strings.TrimSpace(line) == "" {
273 continue
274 } else {
275 ps.Lines = append(ps.Lines, line)
276 }
277 }
278 return ps
279}
diff --git a/six/info_file_parser_test.go b/six/info_file_parser_test.go
new file mode 100644
index 0000000..9994e6f
--- /dev/null
+++ b/six/info_file_parser_test.go
@@ -0,0 +1,256 @@
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (C) 2020 Michael Crute <mike@crute.us>. All rights reserved.
3//
4// Use of this source code is governed by a license that can be found in the
5// LICENSE file.
6
7package six
8
9import (
10 "net"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/stretchr/testify/assert"
16)
17
18var testDate = time.Date(2020, 02, 12, 19, 43, 23, 0, time.UTC)
19
20const roaTest = `
21Timestamp: 2020-02-12 19:43:23 UTC
22Trust Anchor | ASN | Max Length | Prefix
23 APNIC | 64496 | 24 | 192.0.2.0/24
24 RIPE | 64496 | 24 | 198.51.100.0/24
25 RIPE | 64496 | 48 | 2001:db8:3a80::/48
26 APNIC | 64496 | 48 | 2001:db8:3a81::/48
27`
28
29func parseCidr(c string) net.IPNet {
30 _, n, _ := net.ParseCIDR(c)
31 return *n
32}
33
34func TestROAParsing(t *testing.T) {
35 rs := parseROAFile(strings.Split(roaTest, "\n"))
36
37 matrix := []struct {
38 TrustAnchor string
39 ASN int
40 MaxLength int
41 Prefix net.IPNet
42 }{
43 {"APNIC", 64496, 24, parseCidr("192.0.2.0/24")},
44 {"RIPE", 64496, 24, parseCidr("198.51.100.0/24")},
45 {"RIPE", 64496, 48, parseCidr("2001:db8:3a80::/48")},
46 {"APNIC", 64496, 48, parseCidr("2001:db8:3a81::/48")},
47 }
48
49 for i, tv := range matrix {
50 assert.EqualValues(t, tv, rs.ROAS[i])
51 }
52
53 assert.Equal(t, rs.Timestamp, testDate)
54}
55
56const prefixListv4 = `
57Timestamp: 2020-02-12 19:43:23 UTC
58192.0.2.0/24
59198.51.100.0/24
60`
61
62func TestPrefixSetv4Parsing(t *testing.T) {
63 r := parsePrefixSetFile(strings.Split(prefixListv4, "\n"))
64
65 assert.Equal(t, r.Timestamp, testDate)
66 assert.Equal(t, r.Prefixes[0], parseCidr("192.0.2.0/24"))
67 assert.Equal(t, r.Prefixes[1], parseCidr("198.51.100.0/24"))
68}
69
70const prefixListv6 = `
71Timestamp: 2020-02-12 19:43:23 UTC
722001:db8:3a80::/48
732001:db8:3a81::/48
74`
75
76func TestPrefixSetv6Parsing(t *testing.T) {
77 r := parsePrefixSetFile(strings.Split(prefixListv6, "\n"))
78
79 assert.Equal(t, r.Timestamp, testDate)
80 assert.Equal(t, r.Prefixes[0], parseCidr("2001:db8:3a80::/48"))
81 assert.Equal(t, r.Prefixes[1], parseCidr("2001:db8:3a81::/48"))
82}
83
84const asPrefixListv4 = `
85as-set: AS-TEST
86Timestamp: 2020-02-12 19:43:23 UTC
87192.0.2.0/24
88198.51.100.0/24
89198.51.100.0/24,
90`
91
92func TestPrefixSetASNv4Parsing(t *testing.T) {
93 r := parsePrefixSetFile(strings.Split(asPrefixListv4, "\n"))
94
95 assert.Equal(t, r.Timestamp, testDate)
96 assert.Equal(t, *r.ASSet, "AS-TEST")
97 assert.Equal(t, r.Prefixes[0], parseCidr("192.0.2.0/24"))
98 assert.Equal(t, r.Prefixes[1], parseCidr("198.51.100.0/24"))
99 assert.Equal(t, r.Prefixes[2], parseCidr("198.51.100.0/24"))
100}
101
102const asPrefixListv6 = `
103as-set: AS-TEST
104Timestamp: 2020-02-12 19:43:23 UTC
1052001:db8:3a80::/48
1062001:db8:3a81::/48
1072001:db8:3a81::/48,
108`
109
110func TestPrefixSetASNv6Parsing(t *testing.T) {
111 r := parsePrefixSetFile(strings.Split(asPrefixListv6, "\n"))
112
113 assert.Equal(t, r.Timestamp, testDate)
114 assert.Equal(t, *r.ASSet, "AS-TEST")
115 assert.Equal(t, r.Prefixes[0], parseCidr("2001:db8:3a80::/48"))
116 assert.Equal(t, r.Prefixes[1], parseCidr("2001:db8:3a81::/48"))
117 assert.Equal(t, r.Prefixes[2], parseCidr("2001:db8:3a81::/48"))
118}
119
120const asSetFile = `
121as-set: AS-TEST
122Timestamp: 2020-02-12 19:43:23 UTC
12364496
12464497
12564498
126`
127
128func TestASSetParsing(t *testing.T) {
129 r := parseASSetFile(strings.Split(asSetFile, "\n"))
130
131 assert.Equal(t, r.Timestamp, testDate)
132 assert.Equal(t, r.Name, "AS-TEST")
133 assert.Equal(t, r.ASNumbers[0], 64496)
134 assert.Equal(t, r.ASNumbers[1], 64497)
135 assert.Equal(t, r.ASNumbers[2], 64498)
136}
137
138const errorFilev4 = `
1392020-02-12 19:43:23 <INFO> 64496:1:1500:v4:Testnet: 192.0.2.0/24 from 192.0.2.1 (path 64496 64497 64497 64497 64499): AS64499 not member of IRR as-set object! Dropping.
1402020-02-12 19:43:23 <INFO> 64496:1:1500:v4:Testnet: 198.51.100.0/24 from 192.0.2.1: bgp_path(path 64496 64497 64498 64498 64499) matches pdb_never_via_route_servers_ASNs! Dropping.
1412020-02-12 19:43:23 <INFO> 64496:1:1500:v4:Testnet: 192.0.2.0/26 from 192.0.2.1 (path 64496): RPKI ROA_INVALID! Dropping.
142`
143
144const errorFilev6 = `
1452020-02-12 19:43:23 <INFO> 64496:1:1500:v6:Testnet: 2001:db8::/16 from 2001:db8::1: matches bogon_prefixes! Dropping.
1462020-02-12 19:43:23 <INFO> 64496:1:1500:v6:Testnet: 2001:db8::/24 from 2001:db8::1: bgp_path(path 64496 64497 64497 64497 64497 64497 64497 64497 64497 64498) matches pdb_never_via_route_servers_ASNs! Dropping.
1472020-02-12 19:43:23 <INFO> 64496:1:1500:v6:Testnet: 2001:db8::/32 from 2001:db8::1 (path 64496 64497 64498 64498 64499): AS64499 not member of IRR as-set object! Dropping.
148`
149
150// Last line intentionally not parsable
151const errorFilev4j = `
1522020-02-12 19:43:23 <INFO> 64496:1:9000:v4:Testnet: 192.0.2.0/24 from 192.0.2.1: bgp_path(path 64496 64498 { 75757 }) matches bogon_ASNs! Dropping.
1532020-02-12 19:43:23 <INFO> 64496:1:9000:v4:Testnet: 198.51.100.0/24 from 192.0.2.1: matches bogon_prefixes! Dropping.
1542 20-02-12 19:43:23 <INFO> 64496:1:9000:v4:Testnet: 192.0.2.0/26 from 192.0.2.1: matches bogon_prefixes! Dropping.
155`
156
157// Last line intentionally not parsable
158const errorFilev6j = `
1592020-02-12 19:43:23 <INFO> 64496:1:9000:v6:Testnet: 2001:db8::/32 from 2001:db8::1 (path 64496 64497 64499): no IRR route object found! Transit_dropping.
1602020-02-12 19:43:23 <INFO> 64496:1:9000:v6:Testnet: 2001:db8::/36 from 2001:db8::1 (path 64496 64498 64499): no IRR route object found! Transit_dropping.
1612020-02-12 19:43:23 <INFO> 64496:1:9000:v6:Testnet: 2001:db8::/48 from 2001:db8::1 (path 64496 64498 64498 64498 64499): RPKI ROA_INVALID! Transit_dropping.
1622 20-02-12 19:43:23 <INFO> 64496:1:9000:v6:Testnet: 2001:db8::/48 from 2001:db8::1 (path 64496 64498 64498 64498 64499): RPKI ROA_INVALID! Transit_dropping.
163`
164
165func TestParsingErrorFile(t *testing.T) {
166 e := parseErrorFiles(
167 strings.Split(errorFilev4, "\n"),
168 strings.Split(errorFilev6, "\n"),
169 strings.Split(errorFilev4j, "\n"),
170 strings.Split(errorFilev6j, "\n"))
171
172 v4t := []struct {
173 Timestamp time.Time
174 ASN int
175 MTU int
176 Version IPVersion
177 NetworkName string
178 Network net.IPNet
179 Router net.IP
180 Path []int
181 Message string
182 }{
183 {testDate, 64496, 1500, IPv4, "Testnet", parseCidr("192.0.2.0/24"), net.ParseIP("192.0.2.1"), []int{64496, 64497, 64497, 64497, 64499}, "AS64499 not member of IRR as-set object! Dropping."},
184 {testDate, 64496, 1500, IPv4, "Testnet", parseCidr("198.51.100.0/24"), net.ParseIP("192.0.2.1"), []int{64496, 64497, 64498, 64498, 64499}, "matches pdb_never_via_route_servers_ASNs! Dropping."},
185 {testDate, 64496, 1500, IPv4, "Testnet", parseCidr("192.0.2.0/26"), net.ParseIP("192.0.2.1"), []int{64496}, "RPKI ROA_INVALID! Dropping."},
186 }
187
188 for i, tv := range v4t {
189 assert.EqualValues(t, tv, e.IPv4.Records[i])
190 }
191
192 v6t := []struct {
193 Timestamp time.Time
194 ASN int
195 MTU int
196 Version IPVersion
197 NetworkName string
198 Network net.IPNet
199 Router net.IP
200 Path []int
201 Message string
202 }{
203 {testDate, 64496, 1500, IPv6, "Testnet", parseCidr("2001:db8::/16"), net.ParseIP("2001:db8::1"), []int{}, "matches bogon_prefixes! Dropping."},
204 {testDate, 64496, 1500, IPv6, "Testnet", parseCidr("2001:db8::/24"), net.ParseIP("2001:db8::1"), []int{64496, 64497, 64497, 64497, 64497, 64497, 64497, 64497, 64497, 64498}, "matches pdb_never_via_route_servers_ASNs! Dropping."},
205 {testDate, 64496, 1500, IPv6, "Testnet", parseCidr("2001:db8::/32"), net.ParseIP("2001:db8::1"), []int{64496, 64497, 64498, 64498, 64499}, "AS64499 not member of IRR as-set object! Dropping."},
206 }
207
208 for i, tv := range v6t {
209 assert.EqualValues(t, tv, e.IPv6.Records[i])
210 }
211
212 v4jt := []struct {
213 Timestamp time.Time
214 ASN int
215 MTU int
216 Version IPVersion
217 NetworkName string
218 Network net.IPNet
219 Router net.IP
220 Path []int
221 Message string
222 }{
223 {testDate, 64496, 9000, IPv4, "Testnet", parseCidr("192.0.2.0/24"), net.ParseIP("192.0.2.1"), []int{64496, 64498, 75757}, "matches bogon_ASNs! Dropping."},
224 {testDate, 64496, 9000, IPv4, "Testnet", parseCidr("198.51.100.0/24"), net.ParseIP("192.0.2.1"), []int{}, "matches bogon_prefixes! Dropping."},
225 }
226
227 for i, tv := range v4jt {
228 assert.EqualValues(t, tv, e.IPv4Jumbo.Records[i])
229 }
230
231 badLinev4j := "2 20-02-12 19:43:23 <INFO> 64496:1:9000:v4:Testnet: 192.0.2.0/26 from 192.0.2.1: matches bogon_prefixes! Dropping."
232 assert.Equal(t, badLinev4j, e.IPv4Jumbo.UnparsableLines[0])
233
234 v6jt := []struct {
235 Timestamp time.Time
236 ASN int
237 MTU int
238 Version IPVersion
239 NetworkName string
240 Network net.IPNet
241 Router net.IP
242 Path []int
243 Message string
244 }{
245 {testDate, 64496, 9000, IPv6, "Testnet", parseCidr("2001:db8::/32"), net.ParseIP("2001:db8::1"), []int{64496, 64497, 64499}, "no IRR route object found! Transit_dropping."},
246 {testDate, 64496, 9000, IPv6, "Testnet", parseCidr("2001:db8::/36"), net.ParseIP("2001:db8::1"), []int{64496, 64498, 64499}, "no IRR route object found! Transit_dropping."},
247 {testDate, 64496, 9000, IPv6, "Testnet", parseCidr("2001:db8::/48"), net.ParseIP("2001:db8::1"), []int{64496, 64498, 64498, 64498, 64499}, "RPKI ROA_INVALID! Transit_dropping."},
248 }
249
250 for i, tv := range v6jt {
251 assert.EqualValues(t, tv, e.IPv6Jumbo.Records[i])
252 }
253
254 badLinev6j := "2 20-02-12 19:43:23 <INFO> 64496:1:9000:v6:Testnet: 2001:db8::/48 from 2001:db8::1 (path 64496 64498 64498 64498 64499): RPKI ROA_INVALID! Transit_dropping."
255 assert.Equal(t, badLinev6j, e.IPv6Jumbo.UnparsableLines[0])
256}
diff --git a/six/participant.go b/six/participant.go
new file mode 100644
index 0000000..e8150bb
--- /dev/null
+++ b/six/participant.go
@@ -0,0 +1,224 @@
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (C) 2020 Michael Crute <mike@crute.us>. All rights reserved.
3//
4// Use of this source code is governed by a license that can be found in the
5// LICENSE file.
6
7package six
8
9import (
10 "net"
11 "time"
12)
13
14// IP protocol version number
15type IPVersion int
16
17const (
18 IPv6 IPVersion = 6
19 IPv4 = 4
20)
21
22// IPv4 and IPv6 union type, contains a full network for IPv4 and IPv6
23// addresses
24type Addresses struct {
25 IPv4 *net.IPNet
26 IPv6 *net.IPNet
27}
28
29// IRR statistical data. Contains the counts of various IRR data for a
30// participant.
31type IRRData struct {
32 PrefixCount int
33 ASNCount int
34 ASSetCount int
35}
36
37// Set of prefixes, optionally with an AS-SET name that a participant has
38// registered with the exchange.
39type PrefixSet struct {
40 Timestamp time.Time
41 ASSet *string
42 Prefixes []net.IPNet
43}
44
45// Set of AS numbers with an AS-SET that the participant has registered with
46// the exchange.
47type ASSet struct {
48 Timestamp time.Time
49 Name string
50 ASNumbers []int
51}
52
53// Set of ROA record summaries that have been parsed from the raw ROA data for
54// a participant.
55type ROASet struct {
56 Timestamp time.Time
57 ROAS []ROA
58}
59
60// Individual ROA summary
61type ROA struct {
62 TrustAnchor string
63 ASN int
64 MaxLength int
65 Prefix net.IPNet
66}
67
68// A SIX participant is a record of an organization's connection to the Seattle
69// Internet Exchange. It contains information about their connect, the prefixes
70// and ASes that they have registered with the exchange, and their usage of the
71// route server.
72//
73// Not all data exists for all participants, and the data that does exist isn't
74// always in a nice clean format. The parsers attempt to make sense of whatever
75// is available but some data may be missing or inconsistent.
76//
77// The default contains summary data for the participant and more detailed data
78// can be fetched with the methods attached to this struct as well as to the
79// RouteServer structs, if the participant is using the route server.
80type SIXParticipant struct {
81 Organization string
82 URL string
83 ASN int
84 Speed int // Connection speed in Mbit/s
85 Switch string // Connected Switch or Location
86 Contact string // Contact Email
87 Comment string
88 IsConnected bool
89 IsVoter bool
90 Update *time.Time
91 Options []string // Currently: IPv6 and MTU9k
92 PeeringPolicy string
93 ROACount int
94 PeeringDBPrefixCountv4 int
95 PeeringDBPrefixCountv6 int
96 Addresses Addresses
97 JumboAddresses *Addresses
98 IRRv4 IRRData
99 IRRv6 IRRData
100 RouteServer2 *RouteServer
101 RouteServer3 *RouteServer
102}
103
104// Get the set of prefixes that the participant has registered.
105//
106// This does an HTTP fetch to get the detailed file.
107func (p *SIXParticipant) GetPrefixes(ip IPVersion) (*PrefixSet, error) {
108 return fetchParsePrefixList(p.ASN, ip, false)
109}
110
111// Get the prefix to AS associations that the participant has registered.
112//
113// This does an HTTP fetch to get the detailed file.
114func (p *SIXParticipant) GetASPrefixes(ip IPVersion) (*PrefixSet, error) {
115 return fetchParsePrefixList(p.ASN, ip, true)
116}
117
118// Get the AS sets that the participant has registered
119//
120// This does an HTTP fetch to get the detailed file.
121func (p *SIXParticipant) GetASSet(ip IPVersion) (*ASSet, error) {
122 return fetchParseASSet(p.ASN, ip)
123}
124
125// Get the ROA set and more detailed records for the ROAs that the participant
126// has registered. This does not fetch the raw ROAs, just the summary that the
127// exchange has computed.
128//
129// This does an HTTP fetch to get the detailed file.
130func (p *SIXParticipant) GetROASet() (*ROASet, error) {
131 return fetchParseROAFile(p.ASN)
132}
133
134// List of lines from the file containing route data dumped from the route
135// server
136type RouteFile struct {
137 Timestamp time.Time
138 Lines []string
139}
140
141// Lists of errors that the route server has exported. These are parsed for
142// more details if possible, but if that fails the attached ErrorRecords object
143// contains a list of un-parsable lines for consumption.
144type Errors struct {
145 IPv4 *ErrorRecords
146 IPv6 *ErrorRecords
147 IPv4Jumbo *ErrorRecords
148 IPv6Jumbo *ErrorRecords
149}
150
151// List of error records for route server errors. Also contains a list of lines
152// that could not be parsed by the parser.
153type ErrorRecords struct {
154 Records []ErrorRecord
155 UnparsableLines []string
156}
157
158// Create a new ErrorRecords struct
159func NewErrorRecords() *ErrorRecords {
160 return &ErrorRecords{
161 Records: []ErrorRecord{},
162 UnparsableLines: []string{},
163 }
164}
165
166// Contains the parsed data for a route server error.
167type ErrorRecord struct {
168 Timestamp time.Time
169 ASN int
170 MTU int
171 Version IPVersion
172 NetworkName string
173 Network net.IPNet
174 Router net.IP
175 Path []int
176 Message string
177}
178
179// Data about the participant's connection to a route server. If the
180// participant is making use of the route servers this will always contain some
181// basic statistics about their connection. More detailed information can be
182// obtained by calling the methods attached to this struct.
183type RouteServer struct {
184 Number int
185 IPv4 RouteServerStats
186 IPv6 RouteServerStats
187 IPv4Jumbo RouteServerStats
188 IPv6Jumbo RouteServerStats
189 asn int
190}
191
192// Route server statistics. Contains the counts of various types of route
193// server entries.
194type RouteServerStats struct {
195 Prefixes int
196 Errors int
197 TransitErrors int
198}
199
200// Gets the list of routes being advertised by the participant. Note that this
201// file is the raw lines from the route server and no attempt has been made to
202// parse these lines. The lines are in BIRD format.
203//
204// This does an HTTP fetch to get the detailed file.
205func (s *RouteServer) GetRoutes(ip IPVersion, jumbo bool) (*RouteFile, error) {
206 return fetchParseRoutes(s.Number, s.asn, ip, jumbo)
207}
208
209// Gets the errors returned by the route server for all IP protocols and all
210// VLANs to which the participant is connected. If the participant is using
211// multiple route servers, this data is scoped to the current route server.
212//
213// This does an HTTP fetch to get the detailed file.
214func (s *RouteServer) GetErrors() (*Errors, error) {
215 return fetchParseRSErrors(s.Number, s.asn, false), nil
216}
217
218// Get a list of transit errors from the route server. Otherwise return value
219// and behavior is identical to GetErrors.
220//
221// This does an HTTP fetch to get the detailed file.
222func (s *RouteServer) GetTransitErrors() (*Errors, error) {
223 return fetchParseRSErrors(s.Number, s.asn, true), nil
224}
diff --git a/six/participant_builder.go b/six/participant_builder.go
new file mode 100644
index 0000000..f18a6d1
--- /dev/null
+++ b/six/participant_builder.go
@@ -0,0 +1,113 @@
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (C) 2020 Michael Crute <mike@crute.us>. All rights reserved.
3//
4// Use of this source code is governed by a license that can be found in the
5// LICENSE file.
6
7package six
8
9import (
10 "fmt"
11 "strings"
12)
13
14// Create a new SIXParticipant struct from a map of data that was parsed from
15// the participant CSV file. This assumes the column headers from that CSV file
16// so it will not work with the other data formats avaiable.
17//
18// This uses the CSV file because it's both the most rich source of data and
19// the easiest to parse.
20func NewSIXParticipantFromData(d map[string]string) *SIXParticipant {
21 r := &SIXParticipant{
22 Organization: d["Organization"],
23 URL: d["URL"],
24 ASN: mustParseInt(d["ASN"]),
25 Speed: mustParseInt(d["Speed"]),
26 Switch: d["Switch"],
27 Contact: d["Contact"],
28 Comment: d["Comment"],
29 IsConnected: parseYesNo(d["Conn?"]),
30 IsVoter: parseYesNo(d["Voter?"]),
31 Update: mustParseTime(d["Update"]),
32 Options: strings.Split(d["Options"], " "),
33 PeeringPolicy: d["Policy"],
34 ROACount: mustParseInt(d["rpki:roa"]),
35 PeeringDBPrefixCountv4: mustParseInt(d["pdb:v4"]),
36 PeeringDBPrefixCountv6: mustParseInt(d["pdb:v6"]),
37 Addresses: Addresses{
38 IPv4: parseIPNetFromCIDR(d["IPv4"]),
39 IPv6: parseIPNetFromCIDR(d["IPv6"]),
40 },
41 IRRv4: IRRData{
42 PrefixCount: mustParseInt(d["irr:p4"]),
43 ASNCount: mustParseInt(d["irr:a4"]),
44 ASSetCount: mustParseInt(d["irr:ap4"]),
45 },
46 IRRv6: IRRData{
47 PrefixCount: mustParseInt(d["irr:p6"]),
48 ASNCount: mustParseInt(d["irr:a6"]),
49 ASSetCount: mustParseInt(d["irr:ap6"]),
50 },
51 RouteServer2: getRSData(2, d),
52 RouteServer3: getRSData(3, d),
53 }
54
55 // Not all participants use the MTU9k VLAN
56 ja4 := parseIPNetFromCIDR(d["Jumbo IPv4"])
57 ja6 := parseIPNetFromCIDR(d["Jumbo IPv6"])
58 if ja4 != nil && ja6 != nil {
59 r.JumboAddresses = &Addresses{IPv4: ja4, IPv6: ja6}
60 }
61
62 return r
63}
64
65func getRSData(server int, d map[string]string) *RouteServer {
66 // Extract all the data and determine if it's all empty strings, if so then
67 // the participant isn't using the route server. If any data is not empty
68 // then they are. Do integer conversion afterward to avoid ambiguity about
69 // zero vs empty string.
70 pd := []string{
71 d[fmt.Sprintf("rs%d:v4", server)],
72 d[fmt.Sprintf("err%d:v4", server)],
73 d[fmt.Sprintf("xerr%d:v4", server)],
74 d[fmt.Sprintf("rs%d:v6", server)],
75 d[fmt.Sprintf("err%d:v6", server)],
76 d[fmt.Sprintf("xerr%d:v6", server)],
77 d[fmt.Sprintf("rs%d:v4j", server)],
78 d[fmt.Sprintf("err%d:v4j", server)],
79 d[fmt.Sprintf("xerr%d:v4j", server)],
80 d[fmt.Sprintf("rs%d:v6j", server)],
81 d[fmt.Sprintf("err%d:v6j", server)],
82 d[fmt.Sprintf("xerr%d:v6j", server)],
83 }
84
85 if allEmpty(pd) {
86 return nil
87 }
88
89 return &RouteServer{
90 Number: server,
91 asn: mustParseInt(d["ASN"]),
92 IPv4: RouteServerStats{
93 Prefixes: mustParseInt(pd[0]),
94 Errors: mustParseInt(pd[1]),
95 TransitErrors: mustParseInt(pd[2]),
96 },
97 IPv6: RouteServerStats{
98 Prefixes: mustParseInt(pd[3]),
99 Errors: mustParseInt(pd[4]),
100 TransitErrors: mustParseInt(pd[5]),
101 },
102 IPv4Jumbo: RouteServerStats{
103 Prefixes: mustParseInt(pd[6]),
104 Errors: mustParseInt(pd[7]),
105 TransitErrors: mustParseInt(pd[8]),
106 },
107 IPv6Jumbo: RouteServerStats{
108 Prefixes: mustParseInt(pd[9]),
109 Errors: mustParseInt(pd[10]),
110 TransitErrors: mustParseInt(pd[11]),
111 },
112 }
113}