diff options
-rw-r--r-- | LICENSE | 278 | ||||
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | README.rst | 57 | ||||
-rw-r--r-- | cmd/six_monitor/main.go | 250 | ||||
-rw-r--r-- | go.mod | 10 | ||||
-rw-r--r-- | go.sum | 108 | ||||
-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 |
12 files changed, 1854 insertions, 0 deletions
@@ -0,0 +1,278 @@ | |||
1 | GNU GENERAL PUBLIC LICENSE | ||
2 | Version 2, June 1991 | ||
3 | |||
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., | ||
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
6 | Everyone is permitted to copy and distribute verbatim copies | ||
7 | of this license document, but changing it is not allowed. | ||
8 | |||
9 | Preamble | ||
10 | |||
11 | The licenses for most software are designed to take away your | ||
12 | freedom to share and change it. By contrast, the GNU General Public | ||
13 | License is intended to guarantee your freedom to share and change free | ||
14 | software--to make sure the software is free for all its users. This | ||
15 | General Public License applies to most of the Free Software | ||
16 | Foundation's software and to any other program whose authors commit to | ||
17 | using it. (Some other Free Software Foundation software is covered by | ||
18 | the GNU Lesser General Public License instead.) You can apply it to | ||
19 | your programs, too. | ||
20 | |||
21 | When we speak of free software, we are referring to freedom, not | ||
22 | price. Our General Public Licenses are designed to make sure that you | ||
23 | have the freedom to distribute copies of free software (and charge for | ||
24 | this service if you wish), that you receive source code or can get it | ||
25 | if you want it, that you can change the software or use pieces of it | ||
26 | in new free programs; and that you know you can do these things. | ||
27 | |||
28 | To protect your rights, we need to make restrictions that forbid | ||
29 | anyone to deny you these rights or to ask you to surrender the rights. | ||
30 | These restrictions translate to certain responsibilities for you if you | ||
31 | distribute copies of the software, or if you modify it. | ||
32 | |||
33 | For example, if you distribute copies of such a program, whether | ||
34 | gratis or for a fee, you must give the recipients all the rights that | ||
35 | you have. You must make sure that they, too, receive or can get the | ||
36 | source code. And you must show them these terms so they know their | ||
37 | rights. | ||
38 | |||
39 | We protect your rights with two steps: (1) copyright the software, and | ||
40 | (2) offer you this license which gives you legal permission to copy, | ||
41 | distribute and/or modify the software. | ||
42 | |||
43 | Also, for each author's protection and ours, we want to make certain | ||
44 | that everyone understands that there is no warranty for this free | ||
45 | software. If the software is modified by someone else and passed on, we | ||
46 | want its recipients to know that what they have is not the original, so | ||
47 | that any problems introduced by others will not reflect on the original | ||
48 | authors' reputations. | ||
49 | |||
50 | Finally, any free program is threatened constantly by software | ||
51 | patents. We wish to avoid the danger that redistributors of a free | ||
52 | program will individually obtain patent licenses, in effect making the | ||
53 | program proprietary. To prevent this, we have made it clear that any | ||
54 | patent must be licensed for everyone's free use or not licensed at all. | ||
55 | |||
56 | The precise terms and conditions for copying, distribution and | ||
57 | modification follow. | ||
58 | |||
59 | GNU GENERAL PUBLIC LICENSE | ||
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION | ||
61 | |||
62 | 0. This License applies to any program or other work which contains | ||
63 | a notice placed by the copyright holder saying it may be distributed | ||
64 | under the terms of this General Public License. The "Program", below, | ||
65 | refers to any such program or work, and a "work based on the Program" | ||
66 | means either the Program or any derivative work under copyright law: | ||
67 | that is to say, a work containing the Program or a portion of it, | ||
68 | either verbatim or with modifications and/or translated into another | ||
69 | language. (Hereinafter, translation is included without limitation in | ||
70 | the term "modification".) Each licensee is addressed as "you". | ||
71 | |||
72 | Activities other than copying, distribution and modification are not | ||
73 | covered by this License; they are outside its scope. The act of | ||
74 | running the Program is not restricted, and the output from the Program | ||
75 | is covered only if its contents constitute a work based on the | ||
76 | Program (independent of having been made by running the Program). | ||
77 | Whether that is true depends on what the Program does. | ||
78 | |||
79 | 1. You may copy and distribute verbatim copies of the Program's | ||
80 | source code as you receive it, in any medium, provided that you | ||
81 | conspicuously and appropriately publish on each copy an appropriate | ||
82 | copyright notice and disclaimer of warranty; keep intact all the | ||
83 | notices that refer to this License and to the absence of any warranty; | ||
84 | and give any other recipients of the Program a copy of this License | ||
85 | along with the Program. | ||
86 | |||
87 | You may charge a fee for the physical act of transferring a copy, and | ||
88 | you may at your option offer warranty protection in exchange for a fee. | ||
89 | |||
90 | 2. You may modify your copy or copies of the Program or any portion | ||
91 | of it, thus forming a work based on the Program, and copy and | ||
92 | distribute such modifications or work under the terms of Section 1 | ||
93 | above, provided that you also meet all of these conditions: | ||
94 | |||
95 | a) You must cause the modified files to carry prominent notices | ||
96 | stating that you changed the files and the date of any change. | ||
97 | |||
98 | b) You must cause any work that you distribute or publish, that in | ||
99 | whole or in part contains or is derived from the Program or any | ||
100 | part thereof, to be licensed as a whole at no charge to all third | ||
101 | parties under the terms of this License. | ||
102 | |||
103 | c) If the modified program normally reads commands interactively | ||
104 | when run, you must cause it, when started running for such | ||
105 | interactive use in the most ordinary way, to print or display an | ||
106 | announcement including an appropriate copyright notice and a | ||
107 | notice that there is no warranty (or else, saying that you provide | ||
108 | a warranty) and that users may redistribute the program under | ||
109 | these conditions, and telling the user how to view a copy of this | ||
110 | License. (Exception: if the Program itself is interactive but | ||
111 | does not normally print such an announcement, your work based on | ||
112 | the Program is not required to print an announcement.) | ||
113 | |||
114 | These requirements apply to the modified work as a whole. If | ||
115 | identifiable sections of that work are not derived from the Program, | ||
116 | and can be reasonably considered independent and separate works in | ||
117 | themselves, then this License, and its terms, do not apply to those | ||
118 | sections when you distribute them as separate works. But when you | ||
119 | distribute the same sections as part of a whole which is a work based | ||
120 | on the Program, the distribution of the whole must be on the terms of | ||
121 | this License, whose permissions for other licensees extend to the | ||
122 | entire whole, and thus to each and every part regardless of who wrote it. | ||
123 | |||
124 | Thus, it is not the intent of this section to claim rights or contest | ||
125 | your rights to work written entirely by you; rather, the intent is to | ||
126 | exercise the right to control the distribution of derivative or | ||
127 | collective works based on the Program. | ||
128 | |||
129 | In addition, mere aggregation of another work not based on the Program | ||
130 | with the Program (or with a work based on the Program) on a volume of | ||
131 | a storage or distribution medium does not bring the other work under | ||
132 | the scope of this License. | ||
133 | |||
134 | 3. You may copy and distribute the Program (or a work based on it, | ||
135 | under Section 2) in object code or executable form under the terms of | ||
136 | Sections 1 and 2 above provided that you also do one of the following: | ||
137 | |||
138 | a) Accompany it with the complete corresponding machine-readable | ||
139 | source code, which must be distributed under the terms of Sections | ||
140 | 1 and 2 above on a medium customarily used for software interchange; or, | ||
141 | |||
142 | b) Accompany it with a written offer, valid for at least three | ||
143 | years, to give any third party, for a charge no more than your | ||
144 | cost of physically performing source distribution, a complete | ||
145 | machine-readable copy of the corresponding source code, to be | ||
146 | distributed under the terms of Sections 1 and 2 above on a medium | ||
147 | customarily used for software interchange; or, | ||
148 | |||
149 | c) Accompany it with the information you received as to the offer | ||
150 | to distribute corresponding source code. (This alternative is | ||
151 | allowed only for noncommercial distribution and only if you | ||
152 | received the program in object code or executable form with such | ||
153 | an offer, in accord with Subsection b above.) | ||
154 | |||
155 | The source code for a work means the preferred form of the work for | ||
156 | making modifications to it. For an executable work, complete source | ||
157 | code means all the source code for all modules it contains, plus any | ||
158 | associated interface definition files, plus the scripts used to | ||
159 | control compilation and installation of the executable. However, as a | ||
160 | special exception, the source code distributed need not include | ||
161 | anything that is normally distributed (in either source or binary | ||
162 | form) with the major components (compiler, kernel, and so on) of the | ||
163 | operating system on which the executable runs, unless that component | ||
164 | itself accompanies the executable. | ||
165 | |||
166 | If distribution of executable or object code is made by offering | ||
167 | access to copy from a designated place, then offering equivalent | ||
168 | access to copy the source code from the same place counts as | ||
169 | distribution of the source code, even though third parties are not | ||
170 | compelled to copy the source along with the object code. | ||
171 | |||
172 | 4. You may not copy, modify, sublicense, or distribute the Program | ||
173 | except as expressly provided under this License. Any attempt | ||
174 | otherwise to copy, modify, sublicense or distribute the Program is | ||
175 | void, and will automatically terminate your rights under this License. | ||
176 | However, parties who have received copies, or rights, from you under | ||
177 | this License will not have their licenses terminated so long as such | ||
178 | parties remain in full compliance. | ||
179 | |||
180 | 5. You are not required to accept this License, since you have not | ||
181 | signed it. However, nothing else grants you permission to modify or | ||
182 | distribute the Program or its derivative works. These actions are | ||
183 | prohibited by law if you do not accept this License. Therefore, by | ||
184 | modifying or distributing the Program (or any work based on the | ||
185 | Program), you indicate your acceptance of this License to do so, and | ||
186 | all its terms and conditions for copying, distributing or modifying | ||
187 | the Program or works based on it. | ||
188 | |||
189 | 6. Each time you redistribute the Program (or any work based on the | ||
190 | Program), the recipient automatically receives a license from the | ||
191 | original licensor to copy, distribute or modify the Program subject to | ||
192 | these terms and conditions. You may not impose any further | ||
193 | restrictions on the recipients' exercise of the rights granted herein. | ||
194 | You are not responsible for enforcing compliance by third parties to | ||
195 | this License. | ||
196 | |||
197 | 7. If, as a consequence of a court judgment or allegation of patent | ||
198 | infringement or for any other reason (not limited to patent issues), | ||
199 | conditions are imposed on you (whether by court order, agreement or | ||
200 | otherwise) that contradict the conditions of this License, they do not | ||
201 | excuse you from the conditions of this License. If you cannot | ||
202 | distribute so as to satisfy simultaneously your obligations under this | ||
203 | License and any other pertinent obligations, then as a consequence you | ||
204 | may not distribute the Program at all. For example, if a patent | ||
205 | license would not permit royalty-free redistribution of the Program by | ||
206 | all those who receive copies directly or indirectly through you, then | ||
207 | the only way you could satisfy both it and this License would be to | ||
208 | refrain entirely from distribution of the Program. | ||
209 | |||
210 | If any portion of this section is held invalid or unenforceable under | ||
211 | any particular circumstance, the balance of the section is intended to | ||
212 | apply and the section as a whole is intended to apply in other | ||
213 | circumstances. | ||
214 | |||
215 | It is not the purpose of this section to induce you to infringe any | ||
216 | patents or other property right claims or to contest validity of any | ||
217 | such claims; this section has the sole purpose of protecting the | ||
218 | integrity of the free software distribution system, which is | ||
219 | implemented by public license practices. Many people have made | ||
220 | generous contributions to the wide range of software distributed | ||
221 | through that system in reliance on consistent application of that | ||
222 | system; it is up to the author/donor to decide if he or she is willing | ||
223 | to distribute software through any other system and a licensee cannot | ||
224 | impose that choice. | ||
225 | |||
226 | This section is intended to make thoroughly clear what is believed to | ||
227 | be a consequence of the rest of this License. | ||
228 | |||
229 | 8. If the distribution and/or use of the Program is restricted in | ||
230 | certain countries either by patents or by copyrighted interfaces, the | ||
231 | original copyright holder who places the Program under this License | ||
232 | may add an explicit geographical distribution limitation excluding | ||
233 | those countries, so that distribution is permitted only in or among | ||
234 | countries not thus excluded. In such case, this License incorporates | ||
235 | the limitation as if written in the body of this License. | ||
236 | |||
237 | 9. The Free Software Foundation may publish revised and/or new versions | ||
238 | of the General Public License from time to time. Such new versions will | ||
239 | be similar in spirit to the present version, but may differ in detail to | ||
240 | address new problems or concerns. | ||
241 | |||
242 | Each version is given a distinguishing version number. If the Program | ||
243 | specifies a version number of this License which applies to it and "any | ||
244 | later version", you have the option of following the terms and conditions | ||
245 | either of that version or of any later version published by the Free | ||
246 | Software Foundation. If the Program does not specify a version number of | ||
247 | this License, you may choose any version ever published by the Free Software | ||
248 | Foundation. | ||
249 | |||
250 | 10. If you wish to incorporate parts of the Program into other free | ||
251 | programs whose distribution conditions are different, write to the author | ||
252 | to ask for permission. For software which is copyrighted by the Free | ||
253 | Software Foundation, write to the Free Software Foundation; we sometimes | ||
254 | make exceptions for this. Our decision will be guided by the two goals | ||
255 | of preserving the free status of all derivatives of our free software and | ||
256 | of promoting the sharing and reuse of software generally. | ||
257 | |||
258 | NO WARRANTY | ||
259 | |||
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY | ||
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN | ||
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES | ||
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED | ||
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | ||
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS | ||
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE | ||
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, | ||
268 | REPAIR OR CORRECTION. | ||
269 | |||
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR | ||
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, | ||
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING | ||
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED | ||
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY | ||
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER | ||
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE | ||
278 | POSSIBILITY OF SUCH DAMAGES. | ||
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5bd7d42 --- /dev/null +++ b/Makefile | |||
@@ -0,0 +1,8 @@ | |||
1 | SRC_FILES = $(shell find . -name '*.go') | ||
2 | |||
3 | six_status_exporter: test $(SRC_FILES) | ||
4 | go build -o $@ cmd/six_monitor/main.go | ||
5 | |||
6 | .PHONY: test | ||
7 | test: | ||
8 | go test ./... | ||
diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ff27969 --- /dev/null +++ b/README.rst | |||
@@ -0,0 +1,57 @@ | |||
1 | ========================================= | ||
2 | Seattle Internet Exchange Status Reporter | ||
3 | ========================================= | ||
4 | |||
5 | This code is licensed under the GPL version 2.0 only. The code is maintained at | ||
6 | `code.crute.me <https://code.crute.me/pomonaconsulting/six_monitoring>`_ and | ||
7 | mirrored to `GitHub <https://github.com/pomonaconsulting/six_monitoring>`_. | ||
8 | |||
9 | This is a server and library for parsing and reporting on the status of a | ||
10 | participant at the `Seattle Internet Exchange <https://seattleix.net>`_. The | ||
11 | server provides a Prometheus endpoint reporting the stats of a participant's | ||
12 | ASN on the exchange. The stats are gathered from the `participant data`_ | ||
13 | exported by the exchange operators. | ||
14 | |||
15 | The library can be used to process the SIX participants feed as well as to | ||
16 | fetch and parse additional data that is exported by the SIX route servers. It's | ||
17 | mainly useful for generating reports in the case of errors or building | ||
18 | monitoring infrastructure around SIX data. | ||
19 | |||
20 | Installing | ||
21 | ========== | ||
22 | The default ``make`` target will create the correct binary. You can also build | ||
23 | using regular ``go build``, look at the ``Makefile`` for more details. | ||
24 | |||
25 | :: | ||
26 | |||
27 | make | ||
28 | ./six_status_exporter YOUR-ASN | ||
29 | |||
30 | By default the server will run on port ``9119`` and export a Prometheus metrics | ||
31 | endpoint at ``/metrics``. | ||
32 | |||
33 | Library | ||
34 | ======= | ||
35 | The main models are located in `six/participant.go`_ and contain the important | ||
36 | details about the library. To generate the models from the SIX CSV consume the | ||
37 | ``ParseSIXCSV`` and ``FetchParseSIXCSV`` which take a ``io.Reader`` and no | ||
38 | arguments, respectively. Those functions are defined in `six/feed_parser.go`_. | ||
39 | |||
40 | Contributing | ||
41 | ============ | ||
42 | The authors welcome and appreciate contributions. To contribute please open a | ||
43 | pull request on the GitHub page linked above, or email a patch in ``git am`` | ||
44 | format to the author. To have the best contribution experience, please: | ||
45 | |||
46 | * Don't break backwards compatibility of public interfaces | ||
47 | * Write tests for your new feature/bug fix (run ``make test``) | ||
48 | * Ensure that existing tests pass | ||
49 | * Update the readme/documentation, if necessary | ||
50 | * Ensure your code follows ``go fmt`` standards | ||
51 | |||
52 | All code is reviewed before acceptance and changes may be requested to better | ||
53 | follow the conventions of the existing API. | ||
54 | |||
55 | .. _six/participant.go: https://code.crute.me/pomonaconsulting/six_monitoring/tree/six/participant.go | ||
56 | .. _six/feed_parser.go: https://code.crute.me/pomonaconsulting/six_monitoring/tree/six/feed_parser.go | ||
57 | .. _participant data: https://www.seattleix.net/participants/> | ||
diff --git a/cmd/six_monitor/main.go b/cmd/six_monitor/main.go new file mode 100644 index 0000000..cd41d8f --- /dev/null +++ b/cmd/six_monitor/main.go | |||
@@ -0,0 +1,250 @@ | |||
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 main | ||
8 | |||
9 | import ( | ||
10 | "flag" | ||
11 | "fmt" | ||
12 | "log" | ||
13 | "net/http" | ||
14 | _ "net/http/pprof" | ||
15 | "os" | ||
16 | "strconv" | ||
17 | |||
18 | "github.com/prometheus/client_golang/prometheus" | ||
19 | "github.com/prometheus/client_golang/prometheus/promhttp" | ||
20 | "github.com/prometheus/common/version" | ||
21 | |||
22 | "code.crute.me/pomonaconsulting/six_monitoring/six" | ||
23 | ) | ||
24 | |||
25 | const ( | ||
26 | namespace = "sixstatus" | ||
27 | exporter = "six_status_exporter" | ||
28 | ) | ||
29 | |||
30 | var ( | ||
31 | up = prometheus.NewDesc( | ||
32 | prometheus.BuildFQName(namespace, "", "up"), | ||
33 | "Was the SIX query successful?", | ||
34 | nil, nil, | ||
35 | ) | ||
36 | rsPrefixCount = prometheus.NewDesc( | ||
37 | prometheus.BuildFQName(namespace, "route_server", "prefix_count"), | ||
38 | "Number of prefixes advertised to a route server.", | ||
39 | []string{"ipversion", "mtu", "server"}, nil, | ||
40 | ) | ||
41 | rsErrorCount = prometheus.NewDesc( | ||
42 | prometheus.BuildFQName(namespace, "route_server", "error_count"), | ||
43 | "Number of errors reported by a route server.", | ||
44 | []string{"ipversion", "mtu", "server"}, nil, | ||
45 | ) | ||
46 | rsTransitErrorCount = prometheus.NewDesc( | ||
47 | prometheus.BuildFQName(namespace, "route_server", "transit_error_count"), | ||
48 | "Number of transit errors reported by a route server.", | ||
49 | []string{"ipversion", "mtu", "server"}, nil, | ||
50 | ) | ||
51 | irrPrefixCount = prometheus.NewDesc( | ||
52 | prometheus.BuildFQName(namespace, "irr", "prefix_count"), | ||
53 | "Number of prefixes resolved from IRR records.", | ||
54 | []string{"ipversion"}, nil, | ||
55 | ) | ||
56 | irrASNCount = prometheus.NewDesc( | ||
57 | prometheus.BuildFQName(namespace, "irr", "asn_count"), | ||
58 | "Number of ASNs resolved from IRR records.", | ||
59 | []string{"ipversion"}, nil, | ||
60 | ) | ||
61 | irrAsSetPrefixCount = prometheus.NewDesc( | ||
62 | prometheus.BuildFQName(namespace, "irr", "as_set_prefix_count"), | ||
63 | "Number of prefixes in the AS-SET resolved from IRR records.", | ||
64 | []string{"ipversion"}, nil, | ||
65 | ) | ||
66 | pdbPrefixCount = prometheus.NewDesc( | ||
67 | prometheus.BuildFQName(namespace, "peeringdb", "prefix_count"), | ||
68 | "Number of prefixes configured in PeeringDB.", | ||
69 | []string{"ipversion"}, nil, | ||
70 | ) | ||
71 | roaCount = prometheus.NewDesc( | ||
72 | prometheus.BuildFQName(namespace, "", "roa_count"), | ||
73 | "Number of resolved ROA records.", | ||
74 | nil, nil, | ||
75 | ) | ||
76 | ) | ||
77 | |||
78 | type SIXCollector struct { | ||
79 | ASN int | ||
80 | } | ||
81 | |||
82 | var _ prometheus.Collector = (*SIXCollector)(nil) | ||
83 | |||
84 | func (c *SIXCollector) Describe(ch chan<- *prometheus.Desc) { | ||
85 | ch <- up | ||
86 | ch <- rsPrefixCount | ||
87 | ch <- rsErrorCount | ||
88 | ch <- rsTransitErrorCount | ||
89 | ch <- irrPrefixCount | ||
90 | ch <- irrASNCount | ||
91 | ch <- irrAsSetPrefixCount | ||
92 | ch <- pdbPrefixCount | ||
93 | ch <- roaCount | ||
94 | } | ||
95 | |||
96 | func routeServerMetrics(rs *six.RouteServer, ch chan<- prometheus.Metric) { | ||
97 | ch <- prometheus.MustNewConstMetric( | ||
98 | rsPrefixCount, prometheus.GaugeValue, float64(rs.IPv4.Prefixes), | ||
99 | "4", "1500", string(strconv.Itoa(rs.Number)), | ||
100 | ) | ||
101 | ch <- prometheus.MustNewConstMetric( | ||
102 | rsErrorCount, prometheus.GaugeValue, float64(rs.IPv4.Errors), | ||
103 | "4", "1500", string(strconv.Itoa(rs.Number)), | ||
104 | ) | ||
105 | ch <- prometheus.MustNewConstMetric( | ||
106 | rsTransitErrorCount, prometheus.GaugeValue, float64(rs.IPv4.TransitErrors), | ||
107 | "4", "1500", string(strconv.Itoa(rs.Number)), | ||
108 | ) | ||
109 | ch <- prometheus.MustNewConstMetric( | ||
110 | rsPrefixCount, prometheus.GaugeValue, float64(rs.IPv6.Prefixes), | ||
111 | "6", "1500", string(strconv.Itoa(rs.Number)), | ||
112 | ) | ||
113 | ch <- prometheus.MustNewConstMetric( | ||
114 | rsErrorCount, prometheus.GaugeValue, float64(rs.IPv6.Errors), | ||
115 | "6", "1500", string(strconv.Itoa(rs.Number)), | ||
116 | ) | ||
117 | ch <- prometheus.MustNewConstMetric( | ||
118 | rsTransitErrorCount, prometheus.GaugeValue, float64(rs.IPv6.TransitErrors), | ||
119 | "6", "1500", string(strconv.Itoa(rs.Number)), | ||
120 | ) | ||
121 | ch <- prometheus.MustNewConstMetric( | ||
122 | rsPrefixCount, prometheus.GaugeValue, float64(rs.IPv4Jumbo.Prefixes), | ||
123 | "4", "9000", string(strconv.Itoa(rs.Number)), | ||
124 | ) | ||
125 | ch <- prometheus.MustNewConstMetric( | ||
126 | rsErrorCount, prometheus.GaugeValue, float64(rs.IPv4Jumbo.Errors), | ||
127 | "4", "9000", string(strconv.Itoa(rs.Number)), | ||
128 | ) | ||
129 | ch <- prometheus.MustNewConstMetric( | ||
130 | rsTransitErrorCount, prometheus.GaugeValue, float64(rs.IPv4Jumbo.TransitErrors), | ||
131 | "4", "9000", string(strconv.Itoa(rs.Number)), | ||
132 | ) | ||
133 | ch <- prometheus.MustNewConstMetric( | ||
134 | rsPrefixCount, prometheus.GaugeValue, float64(rs.IPv6Jumbo.Prefixes), | ||
135 | "6", "9000", string(strconv.Itoa(rs.Number)), | ||
136 | ) | ||
137 | ch <- prometheus.MustNewConstMetric( | ||
138 | rsErrorCount, prometheus.GaugeValue, float64(rs.IPv6Jumbo.Errors), | ||
139 | "6", "9000", string(strconv.Itoa(rs.Number)), | ||
140 | ) | ||
141 | ch <- prometheus.MustNewConstMetric( | ||
142 | rsTransitErrorCount, prometheus.GaugeValue, float64(rs.IPv6Jumbo.TransitErrors), | ||
143 | "6", "9000", string(strconv.Itoa(rs.Number)), | ||
144 | ) | ||
145 | } | ||
146 | |||
147 | func (c *SIXCollector) Collect(ch chan<- prometheus.Metric) { | ||
148 | sp, err := six.FetchParseSIXCSV() | ||
149 | if err != nil { | ||
150 | log.Printf("error: Fetching and parsing CSV: %s", err) | ||
151 | ch <- prometheus.MustNewConstMetric(up, prometheus.GaugeValue, 0) | ||
152 | return | ||
153 | } | ||
154 | |||
155 | ps, ok := sp.GetParticipantByASN(c.ASN) | ||
156 | if !ok { | ||
157 | log.Printf("error: No participant for ASN: %d", c.ASN) | ||
158 | ch <- prometheus.MustNewConstMetric(up, prometheus.GaugeValue, 0) | ||
159 | return | ||
160 | } | ||
161 | |||
162 | p := ps[0] | ||
163 | |||
164 | ch <- prometheus.MustNewConstMetric(up, prometheus.GaugeValue, 1) | ||
165 | |||
166 | // ipversion, mtu, server | ||
167 | |||
168 | if p.RouteServer2 != nil { | ||
169 | routeServerMetrics(p.RouteServer2, ch) | ||
170 | } | ||
171 | |||
172 | if p.RouteServer3 != nil { | ||
173 | routeServerMetrics(p.RouteServer3, ch) | ||
174 | } | ||
175 | |||
176 | ch <- prometheus.MustNewConstMetric( | ||
177 | irrPrefixCount, prometheus.GaugeValue, float64(p.IRRv4.PrefixCount), | ||
178 | "4", | ||
179 | ) | ||
180 | ch <- prometheus.MustNewConstMetric( | ||
181 | irrASNCount, prometheus.GaugeValue, float64(p.IRRv4.ASNCount), | ||
182 | "4", | ||
183 | ) | ||
184 | ch <- prometheus.MustNewConstMetric( | ||
185 | irrAsSetPrefixCount, prometheus.GaugeValue, float64(p.IRRv4.ASSetCount), | ||
186 | "4", | ||
187 | ) | ||
188 | ch <- prometheus.MustNewConstMetric( | ||
189 | pdbPrefixCount, prometheus.GaugeValue, float64(p.PeeringDBPrefixCountv4), | ||
190 | "4", | ||
191 | ) | ||
192 | ch <- prometheus.MustNewConstMetric( | ||
193 | irrPrefixCount, prometheus.GaugeValue, float64(p.IRRv6.PrefixCount), | ||
194 | "6", | ||
195 | ) | ||
196 | ch <- prometheus.MustNewConstMetric( | ||
197 | irrASNCount, prometheus.GaugeValue, float64(p.IRRv6.ASNCount), | ||
198 | "6", | ||
199 | ) | ||
200 | ch <- prometheus.MustNewConstMetric( | ||
201 | irrAsSetPrefixCount, prometheus.GaugeValue, float64(p.IRRv6.ASSetCount), | ||
202 | "6", | ||
203 | ) | ||
204 | ch <- prometheus.MustNewConstMetric( | ||
205 | pdbPrefixCount, prometheus.GaugeValue, float64(p.PeeringDBPrefixCountv6), | ||
206 | "6", | ||
207 | ) | ||
208 | |||
209 | ch <- prometheus.MustNewConstMetric(roaCount, prometheus.GaugeValue, float64(p.ROACount)) | ||
210 | } | ||
211 | |||
212 | func main() { | ||
213 | var ( | ||
214 | showVersion = flag.Bool("version", false, "Print version information.") | ||
215 | listenAddress = flag.String("web.listen-address", ":9119", "Address to listen on for web interface and telemetry.") | ||
216 | ) | ||
217 | flag.Parse() | ||
218 | |||
219 | if len(os.Args) != 2 { | ||
220 | fmt.Fprintf(os.Stdout, "usage: %s <ASN>\n", os.Args[0]) | ||
221 | os.Exit(1) | ||
222 | } | ||
223 | |||
224 | asn, err := strconv.Atoi(os.Args[1]) | ||
225 | if err != nil { | ||
226 | fmt.Fprintln(os.Stdout, "invalid ASN, must be numeric") | ||
227 | fmt.Fprintf(os.Stdout, "usage: %s <ASN>\n", os.Args[0]) | ||
228 | os.Exit(1) | ||
229 | } | ||
230 | |||
231 | if *showVersion { | ||
232 | fmt.Fprintln(os.Stdout, version.Print(exporter)) | ||
233 | os.Exit(0) | ||
234 | } | ||
235 | log.Println("Starting", exporter, version.Info()) | ||
236 | log.Println("Build context", version.BuildContext()) | ||
237 | |||
238 | prometheus.MustRegister(version.NewCollector(exporter), &SIXCollector{asn}) | ||
239 | |||
240 | log.Println("Starting Server: ", *listenAddress) | ||
241 | http.Handle("/metrics", promhttp.Handler()) | ||
242 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
243 | w.Write([]byte(`<html> | ||
244 | <head><title>SIX Status Exporter</title></head><body> | ||
245 | <h1>SIX Status Exporter</h1> | ||
246 | <p><a href="/metrics">Metrics</a></p> | ||
247 | </body></html>`)) | ||
248 | }) | ||
249 | log.Fatal(http.ListenAndServe(*listenAddress, nil)) | ||
250 | } | ||
@@ -0,0 +1,10 @@ | |||
1 | module code.crute.me/pomonaconsulting/six_monitoring | ||
2 | |||
3 | go 1.13 | ||
4 | |||
5 | require ( | ||
6 | github.com/prometheus-community/bind_exporter v0.3.0 | ||
7 | github.com/prometheus/client_golang v1.4.1 | ||
8 | github.com/prometheus/common v0.9.1 | ||
9 | github.com/stretchr/testify v1.4.0 | ||
10 | ) | ||
@@ -0,0 +1,108 @@ | |||
1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | ||
2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= | ||
3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | ||
4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | ||
5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | ||
6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= | ||
7 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= | ||
8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= | ||
9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||
10 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= | ||
11 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= | ||
12 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= | ||
13 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||
14 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | ||
15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
18 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||
19 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||
20 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | ||
21 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | ||
22 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | ||
23 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||
24 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||
25 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||
26 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= | ||
27 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||
28 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||
29 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||
30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||
31 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||
32 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||
33 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||
34 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | ||
35 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||
36 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||
37 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | ||
38 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||
39 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||
40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||
41 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= | ||
42 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | ||
43 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||
44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||
45 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||
46 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||
47 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | ||
48 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
49 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
52 | github.com/prometheus-community/bind_exporter v0.3.0 h1:DsyFrvT607dHUBbHOh+h8jqNeMGP6+WKZRlZNWNMmpg= | ||
53 | github.com/prometheus-community/bind_exporter v0.3.0/go.mod h1:VNrkjpy+wDFpgW+A/8+CQgMM0fISAfpSdhnat/K6+Ic= | ||
54 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||
55 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= | ||
56 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= | ||
57 | github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8= | ||
58 | github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= | ||
59 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | ||
60 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||
61 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||
62 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= | ||
63 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||
64 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||
65 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= | ||
66 | github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= | ||
67 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= | ||
68 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||
69 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | ||
70 | github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= | ||
71 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= | ||
72 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | ||
73 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= | ||
74 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= | ||
75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
76 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
77 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||
78 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||
79 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||
80 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||
81 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||
82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
83 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
84 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
85 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
86 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
87 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
88 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
89 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
90 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
91 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
92 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
93 | golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
94 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= | ||
95 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
96 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
97 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||
98 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= | ||
99 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= | ||
100 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
101 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
102 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
103 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||
104 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | ||
105 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||
106 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||
107 | gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= | ||
108 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||
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 | } | ||