// SPDX-License-Identifier: GPL-2.0-only // Copyright (C) 2020 Michael Crute . All rights reserved. // // Use of this source code is governed by a license that can be found in the // LICENSE file. package six import ( "fmt" "io/ioutil" "net" "net/http" "regexp" "strings" ) const ( roaUrl = "https://www.seattleix.net/rs/rpki_roas/%d.txt" // ASN pfxUrl = "https://www.seattleix.net/rs/irr_prefixes/%d.v%d.txt" // ASN, IP Version asnUrl = "https://www.seattleix.net/rs/irr_asns/%d.v%d.txt" // ASN, IP Version asSetUrl = "https://www.seattleix.net/rs/irr_as-set_prefixes/%d.v%d.txt" // ASN, IP Version 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 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 ) // Date, ASN, MTU, IP Version, Net Name, Network, Source, AS Path (may have {}), Error Message // See tests for line examples var errorLineRegex = regexp.MustCompile(`(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) <[^>]+> (?P\d+):1:(?P\d+):v(?P[46]):(?P[^:]+): (?P(?:[0-9\.\/]+|[0-9a-f\/:]+)) from (?P(?:[0-9\.]+|[0-9a-f:]+?)):? (?:(?:bgp_path)?\(path (?P[\d {}]+)\):?)? ?(?P.*)`) func fetchParseRSErrors(rsNum, asn int, forTransit bool) *Errors { transit := "" if forTransit { transit = "x" } // Not everyone will have every file and parseErrorFiles ignores nil so // just ignore errors here v4, _ := fetchFile(rsErrUrl, rsNum, 1500, IPv4, asn, 1500, IPv4, transit) v6, _ := fetchFile(rsErrUrl, rsNum, 1500, IPv6, asn, 1500, IPv6, transit) v4j, _ := fetchFile(rsErrUrl, rsNum, 9000, IPv4, asn, 9000, IPv4, transit) v6j, _ := fetchFile(rsErrUrl, rsNum, 9000, IPv6, asn, 9000, IPv6, transit) return parseErrorFiles(v4, v6, v4j, v6j) } func fetchParseRoutes(rsNum, asn int, ipv IPVersion, forJumbo bool) (*RouteFile, error) { mtu := 1500 if forJumbo { mtu = 9000 } f, err := fetchFile(rsPrefixUrl, rsNum, mtu, ipv, asn, mtu, ipv) if err != nil { return nil, err } return parseRoutesFile(f), nil } func fetchParseASSet(asn int, ipv IPVersion) (*ASSet, error) { f, err := fetchFile(asnUrl, asn, ipv) if err != nil { return nil, err } return parseASSetFile(f), nil } func fetchParsePrefixList(asn int, ipv IPVersion, forAsSet bool) (*PrefixSet, error) { url := pfxUrl if forAsSet { url = asSetUrl } f, err := fetchFile(url, asn, ipv) if err != nil { return nil, err } return parsePrefixSetFile(f), nil } func fetchParseROAFile(asn int) (*ROASet, error) { f, err := fetchFile(roaUrl, asn) if err != nil { return nil, err } return parseROAFile(f), nil } func fetchFile(urlTpl string, args ...interface{}) ([]string, error) { r, err := http.Get(fmt.Sprintf(urlTpl, args...)) if err != nil { return nil, err } defer r.Body.Close() b, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err } return strings.Split(string(b), "\n"), nil } func parseROAFile(lines []string) *ROASet { rs := &ROASet{ ROAS: []ROA{}, } for _, line := range lines { if strings.HasPrefix(line, "Timestamp:") { rs.Timestamp = *mustParseLongTime(line[11:]) continue } else if strings.HasPrefix(line, "Trust Anchor") { continue } else if strings.TrimSpace(line) == "" { continue } else { lp := strings.Split(line, "|") rs.ROAS = append(rs.ROAS, ROA{ TrustAnchor: strings.TrimSpace(lp[0]), ASN: mustParseInt(lp[1]), MaxLength: mustParseInt(lp[2]), Prefix: *parseIPNetFromCIDR(lp[3]), }) } } return rs } func parsePrefixSetFile(lines []string) *PrefixSet { ps := &PrefixSet{ Prefixes: []net.IPNet{}, } for _, line := range lines { if strings.HasPrefix(line, "Timestamp:") { ps.Timestamp = *mustParseLongTime(line[11:]) continue } else if strings.HasPrefix(line, "as-set:") { ps.ASSet = &strings.Split(line, " ")[1] continue } else if strings.TrimSpace(line) == "" { continue } else { // Data formatting error on SIX side if strings.HasSuffix(line, ",") { line = line[:len(line)-1] } ps.Prefixes = append(ps.Prefixes, *parseIPNetFromCIDR(line)) } } return ps } func parseASSetFile(lines []string) *ASSet { as := &ASSet{ ASNumbers: []int{}, } for _, line := range lines { if strings.HasPrefix(line, "Timestamp:") { as.Timestamp = *mustParseLongTime(line[11:]) continue } else if strings.HasPrefix(line, "as-set:") { as.Name = strings.Split(line, " ")[1] continue } else if strings.TrimSpace(line) == "" { continue } else { as.ASNumbers = append(as.ASNumbers, mustParseInt(line)) } } return as } func parseErrorFiles(v4, v6, v4j, v6j []string) *Errors { e := &Errors{} if v4 != nil { parseErrorFile(v4, e, IPv4, false) } if v6 != nil { parseErrorFile(v6, e, IPv6, false) } if v4j != nil { parseErrorFile(v4j, e, IPv4, true) } if v6j != nil { parseErrorFile(v6j, e, IPv6, true) } return e } func getTarget(e *Errors, ipv IPVersion, jumbo bool) *ErrorRecords { if ipv == IPv4 && jumbo { if e.IPv4Jumbo == nil { e.IPv4Jumbo = NewErrorRecords() } return e.IPv4Jumbo } else if ipv == IPv6 && jumbo { if e.IPv6Jumbo == nil { e.IPv6Jumbo = NewErrorRecords() } return e.IPv6Jumbo } else if ipv == IPv4 { if e.IPv4 == nil { e.IPv4 = NewErrorRecords() } return e.IPv4 } else if ipv == IPv6 { if e.IPv6 == nil { e.IPv6 = NewErrorRecords() } return e.IPv6 } else { // This should not be possible panic("Could not find target") } } func parseErrorFile(lines []string, e *Errors, ipv IPVersion, jumbo bool) { t := getTarget(e, ipv, jumbo) for _, line := range lines { l := parseErrorLine(line) if l == nil { // Skip blank lines, which sometimes happen if strings.TrimSpace(line) == "" { continue } else { t.UnparsableLines = append(t.UnparsableLines, line) } } else { t.Records = append(t.Records, *l) } } } func parseErrorLine(line string) *ErrorRecord { res := errorLineRegex.FindAllStringSubmatch(line, -1) if len(res) == 0 { return nil } else { row := res[0][1:] return &ErrorRecord{ Timestamp: *mustParseLongTimeNoZone(row[0]), ASN: mustParseInt(row[1]), MTU: mustParseInt(row[2]), Version: IPVersion(mustParseInt(row[3])), NetworkName: row[4], Network: *parseIPNetFromCIDR(row[5]), Router: *parseIP(row[6]), Path: parseASPath(row[7]), Message: row[8], } } } func parseRoutesFile(lines []string) *RouteFile { ps := &RouteFile{ Lines: []string{}, } for _, line := range lines { if strings.HasPrefix(line, "Timestamp:") { ps.Timestamp = *mustParseLongTime(line[11:]) continue } else if strings.TrimSpace(line) == "" { continue } else { ps.Lines = append(ps.Lines, line) } } return ps }