package main
import (
"flag"
"fmt"
"net"
"os"
"strconv"
"strings"
)
var (
txt = flag.Bool("txt", false, "also look up TXT records")
asn = flag.Bool("asn", false, "also look up ASN info from -asn-zone{,4,6} reverse zones")
asnZone = flag.String("asn-zone", "asn.cymru.com", "ASN info zone for -asn")
asnZone4 = flag.String("asn-zone4", "origin.asn.cymru.com", "v4 reverse zone for -asn")
asnZone6 = flag.String("asn-zone6", "origin6.asn.cymru.com", "v6 reverse zone for -asn")
)
func main() {
flag.Parse()
for _, arg := range flag.Args() {
if ipnets, err := resolveArg(arg); err != nil {
fmt.Fprintf(os.Stderr, "# couldn't resolve %s: %s\n", arg, err)
} else {
fmt.Printf("# %s\n", arg)
for _, ipnet := range ipnets {
if *asn {
originInfos, err := lookupOriginInfo(ipnet)
if err != nil {
printErr(ipnet, "origin", err)
} else {
for _, originInfo := range originInfos {
parts := strings.SplitN(originInfo, " | ", 2)
if asn, err := strconv.Atoi(parts[0]); err == nil {
asnNames, err := lookupAsnNames(asn)
if err != nil {
printErr(asn, "asn", err)
} else if len(asnNames) > 0 {
parts[0] += " | " + strings.Join(asnNames, "; ")
}
}
originInfo = strings.Join(parts, " | ")
fmt.Printf("# %s\n", originInfo)
}
}
}
ip := ipnet.IP
for ipnet.Contains(ip) {
if names, err := net.LookupAddr(ip.String()); err != nil {
printErr(ip, "PTR", err)
} else {
fmt.Printf("%s\t%s\n", ip, strings.Join(names, " "))
}
if *txt {
recs, err := net.LookupTXT(arpa(ip))
if err != nil {
printErr(ip, "TXT", err)
} else {
for _, rec := range recs {
fmt.Printf("%s\tTXT %q\n", ip, rec)
}
}
}
ip = next(ip)
}
}
}
}
}
func printErr(what interface{}, rrtype string, err error) {
// the error types here are awful and this feels like it might
// accidentally catch some non-NXDOMAIN errors
if !strings.HasSuffix(err.Error(), "no such host") &&
!strings.HasSuffix(err.Error(), "nodename nor servname provided, or not known") {
fmt.Fprintf(os.Stderr, "%s\t# %s error: %s\n", what, rrtype, err)
}
}
// arpa returns the .in-addr.arpa or .ip6.arpa name corresponding with the given IP.
func arpa(ip net.IP) string {
if ip4 := ip.To4(); ip4 != nil {
return fmt.Sprintf(
"%d.%d.%d.%d.in-addr.arpa",
ip4[3], ip4[2], ip4[1], ip4[0],
)
} else if ip6 := ip.To16(); ip6 != nil {
return fmt.Sprintf(
"%x.%x.%x.%x.%x.%x.%x.%x."+
"%x.%x.%x.%x.%x.%x.%x.%x."+
"%x.%x.%x.%x.%x.%x.%x.%x."+
"%x.%x.%x.%x.%x.%x.%x.%x."+
"ip6.arpa",
ip6[15]&0xf, ip6[15]&0xf0>>4,
ip6[14]&0xf, ip6[14]&0xf0>>4,
ip6[13]&0xf, ip6[13]&0xf0>>4,
ip6[12]&0xf, ip6[12]&0xf0>>4,
ip6[11]&0xf, ip6[11]&0xf0>>4,
ip6[10]&0xf, ip6[10]&0xf0>>4,
ip6[9]&0xf, ip6[9]&0xf0>>4,
ip6[8]&0xf, ip6[8]&0xf0>>4,
ip6[7]&0xf, ip6[7]&0xf0>>4,
ip6[6]&0xf, ip6[6]&0xf0>>4,
ip6[5]&0xf, ip6[5]&0xf0>>4,
ip6[4]&0xf, ip6[4]&0xf0>>4,
ip6[3]&0xf, ip6[3]&0xf0>>4,
ip6[2]&0xf, ip6[2]&0xf0>>4,
ip6[1]&0xf, ip6[1]&0xf0>>4,
ip6[0]&0xf, ip6[0]&0xf0>>4,
)
} else {
panic("non-ip4 non-ip6 ip? " + ip.String())
}
}
// next returns the next IP address after ip, or nil if there is no such address.
func next(ip net.IP) net.IP {
next := make(net.IP, len(ip))
copy(next, ip)
for i := len(next) - 1; i >= 0; i-- {
next[i]++
if next[i] != 0 {
return next
}
}
return nil
}
// resolveArg parses an argument of one of the following forms:
// * 1.2.3.4
// * 1.2.3.4/24
// * 2001::
// * 2001::/16
// * hostname.example.com
// If the given address is a hostname, it is resolved to all of its A and AAAA records.
// It returns the specified networks as a slice of net.IPNet.
func resolveArg(arg string) ([]net.IPNet, error) {
parts := strings.SplitN(arg, "/", 2)
ips, err := resolveHost(parts[0], len(parts) == 1)
if err != nil {
return nil, err
}
ipnets := make([]net.IPNet, len(ips))
for i, ip := range ips {
if len(parts) > 1 {
if _, ipnet, err := net.ParseCIDR(ip.String() + "/" + parts[1]); err != nil {
return nil, err
} else {
ipnets[i] = *ipnet
}
} else {
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
}
ipnets[i].IP = ip
ipnets[i].Mask = net.CIDRMask(len(ip)*8, len(ip)*8)
}
}
return ipnets, nil
}
func resolveHost(host string, canLookup bool) ([]net.IP, error) {
ip := net.ParseIP(host)
if ip != nil {
return []net.IP{ip}, nil
}
if canLookup {
return net.LookupIP(host)
}
return nil, fmt.Errorf("can't parse IP")
}
func lookupOriginInfo(ipnet net.IPNet) ([]string, error) {
ones, bits := ipnet.Mask.Size()
if bits == 0 {
return nil, fmt.Errorf("invalid netmask in %s", ipnet)
}
var name string
if ip4 := ipnet.IP.To4(); ip4 != nil {
for b := (ones-1)/8; b >= 0; b-- {
name += fmt.Sprintf("%d.", ip4[b])
}
name += *asnZone4
} else {
ip6 := ipnet.IP
for n := (ones-1)/4; n >= 0; n-- {
name += fmt.Sprintf("%x.", (ip6[n/2] >> (4-4*(n%2))) & 0xf)
}
name += *asnZone6
}
return net.LookupTXT(name)
}
func lookupAsnNames(asn int) ([]string, error) {
recs, err := net.LookupTXT(fmt.Sprintf("AS%d.%s", asn, *asnZone))
if err != nil {
return nil, err
}
var names []string
for _, rec := range recs {
parts := strings.Split(rec, " | ")
if len(parts) != 5 {
return nil, fmt.Errorf("unknown asn info format for %d: %s", asn, rec)
}
names = append(names, parts[4])
}
return names, nil
}