package main import ( "bufio" "bytes" "errors" "net" "os" "sort" "strconv" "strings" "sync" "time" "github.com/projectdiscovery/goflags" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger/levels" "github.com/projectdiscovery/ipranger" "github.com/projectdiscovery/mapcidr" asn "github.com/projectdiscovery/mapcidr/asn" fileutil "github.com/projectdiscovery/utils/file" sliceutil "github.com/projectdiscovery/utils/slice" updateutils "github.com/projectdiscovery/utils/update" ) // Options contains cli options type Options struct { Slices int HostCount int FileCidr goflags.StringSlice Silent bool Verbose bool Version bool Output string Aggregate bool Shuffle bool ShufflePorts string SkipBaseIP bool SkipBroadcastIP bool AggregateApprox bool SortAscending bool SortDescending bool Count bool FilterIP4 bool FilterIP6 bool ToIP4 bool ToIP6 bool MatchIP goflags.StringSlice FilterIP goflags.StringSlice IPFormats goflags.StringSlice ZeroPadNumberOfZeroes int ZeroPadPermute bool DisableUpdateCheck bool } const banner = ` ____________ ___ __ _ ___ ____ / ___/ _/ _ \/ _ \ / ' \/ _ '/ _ \/ /___/ // // / , _/ /_/_/_/\_,_/ .__/\___/___/____/_/|_| /_/ ` // Version is the current version of mapcidr const version = `v1.1.16` // showBanner is used to show the banner to the user func showBanner() { gologger.Print().Msgf("%s\n", banner) gologger.Print().Msgf("\t\tprojectdiscovery.io\n\n") } // GetUpdateCallback returns a callback function that updates mapcidr func GetUpdateCallback() func() { return func() { showBanner() updateutils.GetUpdateToolCallback("mapcidr", version)() } } // ParseOptions parses the command line options for application func ParseOptions() *Options { options := &Options{} flagSet := goflags.NewFlagSet() flagSet.SetDescription(`mapCIDR is developed to ease load distribution for mass scanning operations, it can be used both as a library and as independent CLI tool.`) // Input flagSet.CreateGroup("input", "Input", flagSet.StringSliceVarP(&options.FileCidr, "cidr", "cl", nil, "CIDR/IP/File containing list of CIDR/IP to process", goflags.FileNormalizedStringSliceOptions), ) // Process flagSet.CreateGroup("process", "Process", flagSet.IntVar(&options.Slices, "sbc", 0, "Slice CIDRs by given CIDR count"), flagSet.IntVar(&options.HostCount, "sbh", 0, "Slice CIDRs by given HOST count"), flagSet.BoolVarP(&options.Aggregate, "aggregate", "a", false, "Aggregate IPs/CIDRs into minimum subnet"), flagSet.BoolVarP(&options.AggregateApprox, "aggregate-approx", "aa", false, "Aggregate sparse IPs/CIDRs into minimum approximated subnet"), flagSet.BoolVarP(&options.Count, "count", "c", false, "Count number of IPs in given CIDR"), flagSet.BoolVarP(&options.ToIP4, "to-ipv4", "t4", false, "Convert IPs to IPv4 format"), flagSet.BoolVarP(&options.ToIP6, "to-ipv6", "t6", false, "Convert IPs to IPv6 format"), flagSet.StringSliceVarP(&options.IPFormats, "if", "ip-format", nil, "IP formats (0,1,2,3,4,5,6,7,8,9,10,11)", goflags.NormalizedStringSliceOptions), flagSet.IntVarP(&options.ZeroPadNumberOfZeroes, "zero-pad-n", "zpn", 3, "number of padded zero to use"), flagSet.BoolVarP(&options.ZeroPadPermute, "zero-pad-permute", "zpp", false, "enable permutations from 0 to zero-pad-n for each octets"), ) // Filter flagSet.CreateGroup("filter", "Filter", flagSet.BoolVarP(&options.FilterIP4, "filter-ipv4", "f4", false, "Filter IPv4 IPs from input"), flagSet.BoolVarP(&options.FilterIP6, "filter-ipv6", "f6", false, "Filter IPv6 IPs from input"), flagSet.BoolVar(&options.SkipBaseIP, "skip-base", false, "Skip base IPs (ending in .0) in output"), flagSet.BoolVar(&options.SkipBroadcastIP, "skip-broadcast", false, "Skip broadcast IPs (ending in .255) in output"), flagSet.StringSliceVarP(&options.MatchIP, "match-ip", "mi", nil, "IP/CIDR/FILE containing list of IP/CIDR to match (comma-separated, file input)", goflags.FileNormalizedStringSliceOptions), flagSet.StringSliceVarP(&options.FilterIP, "filter-ip", "fi", nil, "IP/CIDR/FILE containing list of IP/CIDR to filter (comma-separated, file input)", goflags.FileNormalizedStringSliceOptions), ) // Miscellaneous flagSet.CreateGroup("miscellaneous", "Miscellaneous", flagSet.BoolVarP(&options.SortAscending, "sort", "s", false, "Sort input IPs in ascending order"), flagSet.BoolVarP(&options.SortDescending, "sort-reverse", "sr", false, "Sort input IPs in descending order"), flagSet.BoolVarP(&options.Shuffle, "shuffle-ip", "si", false, "Shuffle Input IPs in random order"), flagSet.StringVarP(&options.ShufflePorts, "shuffle-port", "sp", "", "Shuffle Input IP:Port in random order"), ) //Update flagSet.CreateGroup("update", "Update", flagSet.CallbackVarP(GetUpdateCallback(), "update", "up", "update mapcidr to latest version"), flagSet.BoolVarP(&options.DisableUpdateCheck, "disable-update-check", "duc", false, "disable automatic mapcidr update check"), ) // Output flagSet.CreateGroup("output", "Output", flagSet.BoolVar(&options.Verbose, "verbose", false, "Verbose mode"), flagSet.StringVarP(&options.Output, "output", "o", "", "File to write output to"), flagSet.BoolVar(&options.Silent, "silent", false, "Silent mode"), flagSet.BoolVar(&options.Version, "version", false, "Show version of the project"), ) if err := flagSet.Parse(); err != nil { gologger.Fatal().Msgf("%s\n", err) } // Read the inputs and configure the logging options.configureOutput() showBanner() if options.Version { gologger.Info().Msgf("Current Version: %s\n", version) os.Exit(0) } if !options.DisableUpdateCheck { latestVersion, err := updateutils.GetToolVersionCallback("mapcidr", version)() if err != nil { if options.Verbose { gologger.Error().Msgf("mapcidr version check failed: %v", err.Error()) } } else { gologger.Info().Msgf("Current mapcidr version %v %v", version, updateutils.GetVersionDescription(version, latestVersion)) } } // enable shuffling if ports are specified if len(options.ShufflePorts) > 0 { options.Shuffle = true } // enable all ip encodings if "0" is specified if sliceutil.Contains(options.IPFormats, "0") { options.IPFormats = []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"} } if err := options.validateOptions(); err != nil { gologger.Fatal().Msgf("%s\n", err) } return options } func (options *Options) validateOptions() error { if options.FileCidr == nil && !fileutil.HasStdin() { return errors.New("no input provided") } if options.Slices > 0 && options.HostCount > 0 { return errors.New("sbc and sbh can't be used together") } if options.SortAscending && options.SortDescending { return errors.New("can sort only in one direction") } if options.FilterIP4 && options.FilterIP6 { return errors.New("IP4 and IP6 can't be used together") } if options.ToIP4 && options.ToIP6 { return errors.New("IP4 and IP6 can't be converted together") } if options.FilterIP != nil && options.MatchIP != nil { return errors.New("both match and filter mode specified") } if (options.SortAscending || options.SortDescending) && options.Aggregate { return errors.New("can sort only IPs. sorting can't be used with aggregate") } return nil } // configureOutput configures the output on the screen func (options *Options) configureOutput() { if options.Silent { gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent) } else if options.Verbose { gologger.DefaultLogger.SetMaxLevel(levels.LevelWarning) } } var options *Options func main() { options = ParseOptions() chancidr := make(chan string) outputchan := make(chan string) var wg sync.WaitGroup wg.Add(1) go process(&wg, chancidr, outputchan) wg.Add(1) go output(&wg, outputchan) if fileutil.HasStdin() { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { _ = options.FileCidr.Set(scanner.Text()) } } if options.FileCidr != nil { for _, item := range options.FileCidr { chancidr <- item } } close(chancidr) wg.Wait() } func filterIPsFromFlagList(channel chan string, ip string, ipFlagList []string) { if len(ipFlagList) == 0 { sendToOutputChannel(ip, channel) return } if options.MatchIP != nil { for _, item := range ipFlagList { if strings.EqualFold(ip, item) { sendToOutputChannel(ip, channel) break } } } else if options.FilterIP != nil { var contains = false for _, item := range ipFlagList { if ip == item { contains = true } } if !contains { sendToOutputChannel(ip, channel) } } else { sendToOutputChannel(ip, channel) } } func sendToOutputChannel(ip string, channel chan string) { ipnet := net.ParseIP(ip) switch { case options.ToIP4: if ip4 := ipnet.To4(); ip4 != nil { channel <- ip4.String() } else { channel <- ip } case options.ToIP6: if ip6 := ipnet.To16(); ip6 != nil { // check if it's ip4-mapped-ip6 if ipnet.To4() != nil { channel <- mapcidr.FmtIP4MappedIP6(ip6) } else { channel <- ip6.String() } } else { gologger.Warning().Msgf("%s could not be mapped to IPv6\n", ip) } default: channel <- ip } } func prepareIPsFromCidrFlagList(items []string) []string { var flagIPList []string for _, item := range items { if _, pCidr, err := net.ParseCIDR(item); err == nil && pCidr != nil { if ips, err := mapcidr.IPAddressesAsStream(pCidr.String()); err == nil { for ip := range ips { flagIPList = append(flagIPList, ip) } } } else { flagIPList = append(flagIPList, item) } } return flagIPList } func process(wg *sync.WaitGroup, chancidr, outputchan chan string) { defer wg.Done() var ( allCidrs []*net.IPNet pCidr *net.IPNet ranger *ipranger.IPRanger err error hasSort = options.SortAscending || options.SortDescending ipRangeList = make([][]net.IP, 0) asnNumberList []string ) ranger, _ = ipranger.New() for cidr := range chancidr { // Add IPs into ipRangeList which are passed as input. Example - "192.168.0.0-192.168.0.5" if strings.Contains(cidr, "-") { var ipRange []net.IP for _, ipstr := range strings.Split(cidr, "-") { ipRange = append(ipRange, net.ParseIP(ipstr)) } //check if ipRange has more than 2 values if len(ipRange) > 2 { gologger.Fatal().Msgf("IP range can not have more than 2 values.") } ipRangeList = append(ipRangeList, ipRange) continue } // Add ASN number if asn.IsASN(cidr) { asnNumberList = append(asnNumberList, cidr) continue } // if it's an ip turn it into a cidr if ip := net.ParseIP(cidr); ip != nil { switch { case ip.To4() != nil: cidr += "/32" case ip.To16() != nil: cidr += "/128" } } // test if we have a cidr if _, pCidr, err = net.ParseCIDR(cidr); err != nil { gologger.Fatal().Msgf("%s\n", err) } // filters ip4|ip6, by default do not filter _, bits := pCidr.Mask.Size() isCidr4 := bits == mapcidr.DefaultMaskSize4 isCidr6 := bits > mapcidr.DefaultMaskSize4 isWrongIpType := (options.FilterIP4 && isCidr6) || (options.FilterIP6 && isCidr4) if isWrongIpType { continue } // In case of coalesce/shuffle we need to know all the cidrs and aggregate them by calling the proper function if options.Aggregate || options.Shuffle || hasSort || options.AggregateApprox || options.Count { _ = ranger.Add(cidr) allCidrs = append(allCidrs, pCidr) } else { commonFunc(cidr, outputchan) } } for _, ipRange := range ipRangeList { cidrs, err := mapcidr.GetCIDRFromIPRange(ipRange[0], ipRange[1]) if err != nil { gologger.Fatal().Msgf("%s\n", err) } if options.Aggregate || options.Shuffle || hasSort || options.AggregateApprox || options.Count { allCidrs = append(allCidrs, cidrs...) } else { for _, cidr := range cidrs { commonFunc(cidr.String(), outputchan) } } } for _, asnNumber := range asnNumberList { cidrs, err := asn.GetCIDRsForASNNum(asnNumber) if err != nil { gologger.Fatal().Msgf("%s\n", err) } if options.Aggregate || options.Shuffle || hasSort || options.AggregateApprox || options.Count { allCidrs = append(allCidrs, cidrs...) } else { for _, cidr := range cidrs { commonFunc(cidr.String(), outputchan) } } } // Shuffle perform the aggregation if options.Shuffle { var ports []int if options.ShufflePorts != "" { for _, p := range strings.Split(options.ShufflePorts, ",") { port, err := strconv.Atoi(p) if err != nil { gologger.Fatal().Msgf("%s\n", err) } ports = append(ports, port) } } cCidrsIPV4, _ := mapcidr.CoalesceCIDRs(allCidrs) if len(ports) > 0 { for ip := range mapcidr.ShuffleCidrsWithPortsAndSeed(cCidrsIPV4, ports, time.Now().Unix()) { outputchan <- ip.String() } } else { for ip := range mapcidr.ShuffleCidrsWithSeed(cCidrsIPV4, time.Now().Unix()) { outputchan <- ip.IP } } } // Aggregate all ips into the minimal subset possible if options.Aggregate { cCidrsIPV4, cCidrsIPV6 := mapcidr.CoalesceCIDRs(allCidrs) for _, cidrIPV4 := range cCidrsIPV4 { outputchan <- cidrIPV4.String() } for _, cidrIPV6 := range cCidrsIPV6 { outputchan <- cidrIPV6.String() } } if hasSort { ips := getIPList(allCidrs) if options.SortDescending { sort.Slice(ips, func(i, j int) bool { return bytes.Compare(ips[j], ips[i]) < 0 }) } else { sort.Slice(ips, func(i, j int) bool { return bytes.Compare(ips[i], ips[j]) < 0 }) } for _, ip := range ips { outputchan <- ip.String() } } if options.AggregateApprox { for _, cidr := range mapcidr.AggregateApproxIPV4s(allCidrs) { outputchan <- cidr.String() } } if options.Count { includeBase := !options.SkipBaseIP includeBroadcast := !options.SkipBroadcastIP ipSum := mapcidr.CountIPsInCIDRs(includeBase, includeBroadcast, allCidrs...) outputchan <- ipSum.String() } close(outputchan) } /* The purpose of the function is split into subnets or split by no. of host or CIDR expansion. This gives us benefit of DRY and we can add new features here going forward. */ func commonFunc(cidr string, outputchan chan string) { if options.Slices > 0 { subnets, err := mapcidr.SplitN(cidr, options.Slices) if err != nil { gologger.Fatal().Msgf("%s\n", err) } for _, subnet := range subnets { outputchan <- subnet.String() } } else if options.HostCount > 0 { subnets, err := mapcidr.SplitByNumber(cidr, options.HostCount) if err != nil { gologger.Fatal().Msgf("%s\n", err) } for _, subnet := range subnets { outputchan <- subnet.String() } } else { var ipFlagList []string ipFlagList = append(ipFlagList, prepareIPsFromCidrFlagList(options.MatchIP)...) ipFlagList = append(ipFlagList, prepareIPsFromCidrFlagList(options.FilterIP)...) ips, err := mapcidr.IPAddressesAsStream(cidr) if err != nil { gologger.Fatal().Msgf("%s\n", err) } for ip := range ips { filterIPsFromFlagList(outputchan, ip, ipFlagList) } } } func output(wg *sync.WaitGroup, outputchan chan string) { defer wg.Done() var f *os.File if options.Output != "" { var err error f, err = os.Create(options.Output) if err != nil { gologger.Fatal().Msgf("Could not create output file '%s': %s\n", options.Output, err) } defer f.Close() //nolint } for o := range outputchan { if o == "" { continue } if options.SkipBaseIP && mapcidr.IsBaseIP(o) { continue } if options.SkipBroadcastIP && mapcidr.IsBroadcastIP(o) { continue } if len(options.IPFormats) > 0 { outputItems(f, mapcidr.AlterIP(o, options.IPFormats, options.ZeroPadNumberOfZeroes, options.ZeroPadPermute)...) } else { outputItems(f, o) } } } func outputItems(f *os.File, items ...string) { for _, item := range items { gologger.Silent().Msgf("%s\n", item) if f != nil { _, _ = f.WriteString(item + "\n") } } } // returns the list of expanded IPs of given CIDR list func getIPList(cidrs []*net.IPNet) []net.IP { var ipList []net.IP for _, cidr := range cidrs { ips, err := mapcidr.IPAddressesAsStream(cidr.String()) if err != nil { gologger.Fatal().Msgf("%s\n", err) } for ip := range ips { ipList = append(ipList, net.ParseIP(ip)) } } return ipList }