diff options
author | Mike Crute <mike@crute.us> | 2020-02-13 06:02:52 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2020-02-13 06:21:45 +0000 |
commit | 651151bce6be01a5cef0e8eb400e0a028b93d887 (patch) | |
tree | bc4dcf69fea04502df198117395a804db60595d8 /six | |
download | six_monitoring-651151bce6be01a5cef0e8eb400e0a028b93d887.tar.bz2 six_monitoring-651151bce6be01a5cef0e8eb400e0a028b93d887.tar.xz six_monitoring-651151bce6be01a5cef0e8eb400e0a028b93d887.zip |
Diffstat (limited to 'six')
-rw-r--r-- | six/feed_parser.go | 159 | ||||
-rw-r--r-- | six/helpers.go | 112 | ||||
-rw-r--r-- | six/info_file_parser.go | 279 | ||||
-rw-r--r-- | six/info_file_parser_test.go | 256 | ||||
-rw-r--r-- | six/participant.go | 224 | ||||
-rw-r--r-- | six/participant_builder.go | 113 |
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 | |||
7 | package six | ||
8 | |||
9 | import ( | ||
10 | "encoding/csv" | ||
11 | "fmt" | ||
12 | "io" | ||
13 | "net/http" | ||
14 | "os" | ||
15 | ) | ||
16 | |||
17 | const 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. | ||
22 | type SIXParticipantParseError struct { | ||
23 | Line int | ||
24 | Err error | ||
25 | } | ||
26 | |||
27 | // Return the error string for the error | ||
28 | func (e *SIXParticipantParseError) Error() string { | ||
29 | return fmt.Sprintf("record on line %d: %s", e.Line, e.Err) | ||
30 | } | ||
31 | |||
32 | // Returns the wrapped error | ||
33 | func (e *SIXParticipantParseError) Unwrap() error { | ||
34 | return e.Err | ||
35 | } | ||
36 | |||
37 | // Parser for the SIX participant CSV feed. | ||
38 | type SIXParser struct { | ||
39 | Records []*SIXParticipant | ||
40 | Errors []error | ||
41 | asnIndex map[int][]*SIXParticipant | ||
42 | headers []string | ||
43 | } | ||
44 | |||
45 | // Builds a new SIXParser | ||
46 | func 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. | ||
59 | func (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 | ||
65 | func (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 | |||
75 | func (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 | |||
83 | func (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. | ||
102 | func (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. | ||
111 | func (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. | ||
133 | func 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. | ||
148 | func 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 | |||
7 | package six | ||
8 | |||
9 | import ( | ||
10 | "net" | ||
11 | "strconv" | ||
12 | "strings" | ||
13 | "time" | ||
14 | ) | ||
15 | |||
16 | func 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 | |||
31 | func parseYesNo(b string) bool { | ||
32 | return strings.ToLower(strings.TrimSpace(b)) == "yes" | ||
33 | } | ||
34 | |||
35 | func parseIP(i string) *net.IP { | ||
36 | o := net.ParseIP(i) | ||
37 | return &o | ||
38 | } | ||
39 | |||
40 | func parseIPNetFromCIDR(i string) *net.IPNet { | ||
41 | _, ipnet, _ := net.ParseCIDR(strings.TrimSpace(i)) | ||
42 | return ipnet | ||
43 | } | ||
44 | |||
45 | func 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 | |||
60 | func 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 | |||
75 | func 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 | |||
90 | func allEmpty(d []string) bool { | ||
91 | for _, v := range d { | ||
92 | if strings.TrimSpace(v) != "" { | ||
93 | return false | ||
94 | } | ||
95 | } | ||
96 | return true | ||
97 | } | ||
98 | |||
99 | func 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 | |||
7 | package six | ||
8 | |||
9 | import ( | ||
10 | "fmt" | ||
11 | "io/ioutil" | ||
12 | "net" | ||
13 | "net/http" | ||
14 | "regexp" | ||
15 | "strings" | ||
16 | ) | ||
17 | |||
18 | const ( | ||
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 | ||
29 | var 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 | |||
31 | func 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 | |||
47 | func 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 | |||
61 | func 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 | |||
69 | func 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 | |||
82 | func 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 | |||
90 | func 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 | |||
105 | func 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 | |||
132 | func 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 | |||
158 | func 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 | |||
180 | func 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 | |||
199 | func 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 | |||
226 | func 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 | |||
244 | func 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 | |||
264 | func 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 | |||
7 | package six | ||
8 | |||
9 | import ( | ||
10 | "net" | ||
11 | "strings" | ||
12 | "testing" | ||
13 | "time" | ||
14 | |||
15 | "github.com/stretchr/testify/assert" | ||
16 | ) | ||
17 | |||
18 | var testDate = time.Date(2020, 02, 12, 19, 43, 23, 0, time.UTC) | ||
19 | |||
20 | const roaTest = ` | ||
21 | Timestamp: 2020-02-12 19:43:23 UTC | ||
22 | Trust 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 | |||
29 | func parseCidr(c string) net.IPNet { | ||
30 | _, n, _ := net.ParseCIDR(c) | ||
31 | return *n | ||
32 | } | ||
33 | |||
34 | func 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 | |||
56 | const prefixListv4 = ` | ||
57 | Timestamp: 2020-02-12 19:43:23 UTC | ||
58 | 192.0.2.0/24 | ||
59 | 198.51.100.0/24 | ||
60 | ` | ||
61 | |||
62 | func 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 | |||
70 | const prefixListv6 = ` | ||
71 | Timestamp: 2020-02-12 19:43:23 UTC | ||
72 | 2001:db8:3a80::/48 | ||
73 | 2001:db8:3a81::/48 | ||
74 | ` | ||
75 | |||
76 | func 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 | |||
84 | const asPrefixListv4 = ` | ||
85 | as-set: AS-TEST | ||
86 | Timestamp: 2020-02-12 19:43:23 UTC | ||
87 | 192.0.2.0/24 | ||
88 | 198.51.100.0/24 | ||
89 | 198.51.100.0/24, | ||
90 | ` | ||
91 | |||
92 | func 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 | |||
102 | const asPrefixListv6 = ` | ||
103 | as-set: AS-TEST | ||
104 | Timestamp: 2020-02-12 19:43:23 UTC | ||
105 | 2001:db8:3a80::/48 | ||
106 | 2001:db8:3a81::/48 | ||
107 | 2001:db8:3a81::/48, | ||
108 | ` | ||
109 | |||
110 | func 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 | |||
120 | const asSetFile = ` | ||
121 | as-set: AS-TEST | ||
122 | Timestamp: 2020-02-12 19:43:23 UTC | ||
123 | 64496 | ||
124 | 64497 | ||
125 | 64498 | ||
126 | ` | ||
127 | |||
128 | func 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 | |||
138 | const errorFilev4 = ` | ||
139 | 2020-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. | ||
140 | 2020-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. | ||
141 | 2020-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 | |||
144 | const errorFilev6 = ` | ||
145 | 2020-02-12 19:43:23 <INFO> 64496:1:1500:v6:Testnet: 2001:db8::/16 from 2001:db8::1: matches bogon_prefixes! Dropping. | ||
146 | 2020-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. | ||
147 | 2020-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 | ||
151 | const errorFilev4j = ` | ||
152 | 2020-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. | ||
153 | 2020-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. | ||
154 | 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. | ||
155 | ` | ||
156 | |||
157 | // Last line intentionally not parsable | ||
158 | const errorFilev6j = ` | ||
159 | 2020-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. | ||
160 | 2020-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. | ||
161 | 2020-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. | ||
162 | 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. | ||
163 | ` | ||
164 | |||
165 | func 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 | |||
7 | package six | ||
8 | |||
9 | import ( | ||
10 | "net" | ||
11 | "time" | ||
12 | ) | ||
13 | |||
14 | // IP protocol version number | ||
15 | type IPVersion int | ||
16 | |||
17 | const ( | ||
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 | ||
24 | type 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. | ||
31 | type 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. | ||
39 | type 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. | ||
47 | type 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. | ||
55 | type ROASet struct { | ||
56 | Timestamp time.Time | ||
57 | ROAS []ROA | ||
58 | } | ||
59 | |||
60 | // Individual ROA summary | ||
61 | type 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. | ||
80 | type 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. | ||
107 | func (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. | ||
114 | func (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. | ||
121 | func (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. | ||
130 | func (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 | ||
136 | type 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. | ||
144 | type 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. | ||
153 | type ErrorRecords struct { | ||
154 | Records []ErrorRecord | ||
155 | UnparsableLines []string | ||
156 | } | ||
157 | |||
158 | // Create a new ErrorRecords struct | ||
159 | func NewErrorRecords() *ErrorRecords { | ||
160 | return &ErrorRecords{ | ||
161 | Records: []ErrorRecord{}, | ||
162 | UnparsableLines: []string{}, | ||
163 | } | ||
164 | } | ||
165 | |||
166 | // Contains the parsed data for a route server error. | ||
167 | type 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. | ||
183 | type 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. | ||
194 | type 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. | ||
205 | func (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. | ||
214 | func (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. | ||
222 | func (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 | |||
7 | package six | ||
8 | |||
9 | import ( | ||
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. | ||
20 | func 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 | |||
65 | func 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 | } | ||