diff options
Diffstat (limited to 'six/info_file_parser.go')
-rw-r--r-- | six/info_file_parser.go | 279 |
1 files changed, 279 insertions, 0 deletions
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 | } | ||