diff --git a/README.md b/README.md index 06d0276..51bc44c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,184 @@ # lmap -lmap (LinuxHub's Nmap) is the nmap next generation pro plus max, made by 浪神 (from THE GREAT [LinuxHub](https://github.com/LinuxHub-Group)). +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -# LICENSE +

LinuxHub's Nmap - 下一代网络扫描工具

- Copyright (C) <2021> +

+ 比 nmap 更强大、更灵活的网络探测与扫描能力 +
+ 探索 lmap 的功能 » +
+
+ 报告 Bug + · + 请求新功能 + · + 贡献 +

- This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +## 目录 - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. +- [关于项目](#关于项目) +- [功能特性](#功能特性) +- [快速开始](#快速开始) + - [安装](#安装) + - [使用方法](#使用方法) +- [构建选项](#构建选项) +- [贡献](#贡献) +- [许可证](#许可证) - You should have received a copy of the GNU General Public License - along with this program. If not, see . +## 关于项目 + +lmap (LinuxHub's Nmap) 是 LinuxHub 团队开发的下一代网络扫描工具,被称为 nmap 的 Pro Plus Max 版本。它是一个现代化的网络扫描工具,旨在提供比传统 nmap 更强大、更灵活的网络探测与扫描能力。 + +该工具专为网络安全工程师、系统管理员和渗透测试人员设计,帮助他们快速识别网络中的活动主机、开放端口和服务。 + +### 为什么选择 lmap? + +- **快速扫描** - 利用 Go 语言的并发特性,实现高速网络扫描 +- **跨平台支持** - 支持 Windows、Linux、macOS 等多种操作系统 +- **易于使用** - 简洁的命令行界面,直观的参数设置 +- **高度可定制** - 支持灵活的排除规则和扫描配置 +- **开源免费** - 基于 GPL-3.0 许可证,完全开源 + +## 功能特性 + +- [x] 🚀 **网络扫描** - 快速扫描指定网络段中的活动主机 +- [x] ✅ **IP检查** - 验证IP地址有效性并检查主机是否在线 +- [x] 🔍 **子网解析** - 解析CIDR格式的子网并生成IP地址列表 +- [x] 📡 **ICMP监听** - 监听网络中的ICMP数据包 +- [x] 🏓 **Ping探测** - 使用ICMP协议探测主机是否在线 +- [x] ⚡ **并发处理** - 支持高并发扫描,提高扫描效率 +- [x] 🚫 **排除规则** - 支持排除特定IP或子网,避免扫描不必要目标 +- [x] 📋 **详细输出** - 提供详细的扫描过程信息 + +## 快速开始 + +### 安装 + +#### 预编译二进制文件 + +从 [Releases](https://github.com/LinuxHub-Group/lmap/releases) 页面下载适用于您系统的预编译二进制文件。 + +#### 从源码构建 + +**要求**: +- Go 1.16 或更高版本 + +```bash +# 克隆项目 +git clone https://github.com/LinuxHub-Group/lmap.git +cd lmap + +# 构建 +make + +# 或者直接使用Go构建 +go build ./cmd/lmap +``` + +构建完成后,您将在项目根目录下获得 [lmap](file:///D:/works/lmap/lmap/lmap.exe) 可执行文件。 + +### 使用方法 + +#### 基本扫描 + +```bash +# 扫描单个子网 +./lmap -subnet 192.168.1.0/24 + +# 扫描多个子网 +./lmap -subnet 192.168.1.0/24 -subnet 10.0.0.0/16 + +# 详细输出模式 +./lmap -subnet 192.168.1.0/24 -v +``` + +#### 排除特定IP或子网 + +```bash +# 排除单个IP +./lmap -subnet 192.168.1.0/24 -exclude 192.168.1.1 + +# 排除多个IP或子网 +./lmap -subnet 192.168.1.0/24 -exclude 192.168.1.1 -exclude 192.168.1.10/32 +``` + +#### 命令行选项 + +| 选项 | 描述 | 示例 | +|------|------|------| +| `-subnet` | 要扫描的网络段,CIDR格式 (可多次指定) | `-subnet 192.168.1.0/24` | +| `-exclude` | 要排除的IP或子网 (可多次指定) | `-exclude 192.168.1.1` | +| `-v` | 详细输出模式 | `-v` | + +## 构建选项 + +```bash +# 默认构建当前平台版本 +make + +# 构建所有平台版本 +make all + +# 格式化代码 +make fmt + +# 运行测试 +make test + +# 清理构建产物 +make clean +``` + +## 贡献 + +欢迎任何形式的贡献!如果您想为 lmap 做出贡献,请遵循以下步骤: + +1. Fork 项目 +2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 开启一个 Pull Request + +### 开发环境设置 + +```bash +# 1. 克隆项目 +git clone https://github.com/LinuxHub-Group/lmap.git + +# 2. 进入项目目录 +cd lmap + +# 3. 安装依赖 +make install + +# 4. 运行测试 +make test +``` + +## 社区和支持 + +- [报告 Bug](https://github.com/LinuxHub-Group/lmap/issues) +- [请求新功能](https://github.com/LinuxHub-Group/lmap/issues) +- [查看已知问题](https://github.com/LinuxHub-Group/lmap/issues) + +## 许可证 + +``` +Copyright (C) <2021> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +``` \ No newline at end of file diff --git a/cmd/lmap/main.go b/cmd/lmap/main.go index 8f1da3f..cc221b5 100644 --- a/cmd/lmap/main.go +++ b/cmd/lmap/main.go @@ -28,12 +28,51 @@ import ( func main() { isVerbose := false - flag.BoolVar(&isVerbose, "v", false, "be verbose") + flag.BoolVar(&isVerbose, "v", false, "详细输出") + + subnets := multiArg{} + flag.Var(&subnets, "subnet", "要扫描的网络段,CIDR格式 (可多次指定)") + + excludes := multiArg{} + flag.Var(&excludes, "exclude", "要排除的IP或子网 (可多次指定)") + flag.Parse() - args := flag.Args() - if len(args) < 1 { - _, _ = fmt.Fprintf(os.Stderr, "使用方法:%s [-v] <网络号>/\n", os.Args[0]) - os.Exit(-1) + + if len(subnets) < 1 { + printUsage() + os.Exit(1) + } + + lmap.CheckIP(subnets, excludes, isVerbose) +} + +// printUsage prints the usage information for the program +func printUsage() { + progName := os.Args[0] + if progName == "" { + progName = "lmap" } - lmap.CheckIP(args[0], isVerbose) + + fmt.Fprintf(os.Stderr, "使用方法: %s [选项] -subnet <网络号>/ [-subnet ...]\n", progName) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "选项:") + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "示例:") + fmt.Fprintf(os.Stderr, " %s -subnet 192.168.1.0/24\n", progName) + fmt.Fprintf(os.Stderr, " %s -subnet 192.168.1.0/24 -subnet 10.0.0.0/16\n", progName) + fmt.Fprintf(os.Stderr, " %s -subnet 192.168.1.0/24 -exclude 192.168.1.1\n", progName) + fmt.Fprintf(os.Stderr, " %s -subnet 192.168.1.0/24 -exclude 192.168.1.10/32 -v\n", progName) +} + +// multiArg implements the flag.Value interface for collecting multiple string values +type multiArg []string + +func (m *multiArg) String() string { + return fmt.Sprintf("%v", *m) +} + +func (m *multiArg) Set(value string) error { + *m = append(*m, value) + return nil } diff --git a/go.mod b/go.mod index 2dc3fc7..6609805 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,23 @@ module github.com/LinuxHub-Group/lmap -go 1.16 +go 1.23.0 require ( - github.com/ysmood/got v0.30.0 - golang.org/x/net v0.0.0-20220607020251-c690dde0001d + github.com/ysmood/got v0.41.0 + golang.org/x/net v0.43.0 +) + +require ( + github.com/google/go-cmp v0.6.0 // indirect + github.com/ysmood/gop v0.2.0 // indirect + github.com/yuin/goldmark v1.4.13 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect ) diff --git a/go.sum b/go.sum index 5e95a94..640a7f4 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,28 @@ +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= github.com/ysmood/got v0.30.0 h1:n6KVknQ2gjU2FsEVvMrpEw20uoqkhRDvAoNK69bOV/U= github.com/ysmood/got v0.30.0/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/got v0.41.0 h1:XiFH311ltTSGyxjeKcNvy7dzbJjjTzn6DBgK313JHBs= +github.com/ysmood/got v0.41.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/lmap/check_ip.go b/pkg/lmap/check_ip.go index 1517663..368f461 100644 --- a/pkg/lmap/check_ip.go +++ b/pkg/lmap/check_ip.go @@ -25,39 +25,64 @@ import ( "time" ) -const OUTPUT_IP_PER_LINE = 3 +const ( + OUTPUT_IP_PER_LINE = 3 + MAX_CONCURRENT_PINGS = 100 +) -func CheckIP(subnet string, isVerbose bool) { +// CheckIP scans the given subnets for active hosts +// subnets: list of CIDR notation networks to scan +// excludes: list of IPs or subnets to exclude from scanning +// isVerbose: if true, print detailed information during scanning +func CheckIP(subnets, excludes []string, isVerbose bool) { checkerGroup := &sync.WaitGroup{} t := time.Now() - hosts, _ := GetAllIPsFromCIDR(subnet) - for index := range hosts { - //time.Sleep(500) + + // Use a semaphore to limit concurrent pings + semaphore := make(chan struct{}, MAX_CONCURRENT_PINGS) + + var allHosts []HostInfo + for _, subnet := range subnets { + hosts, err := GetAllIPsFromCIDR(subnet, excludes) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Error parsing subnet %s: %v\n", subnet, err) + continue + } + allHosts = append(allHosts, hosts...) + } + + for index := range allHosts { checkerGroup.Add(1) + semaphore <- struct{}{} // Acquire semaphore go func(index int) { defer checkerGroup.Done() - hosts[index].isUsed = Ping(hosts[index].host) + defer func() { <-semaphore }() // Release semaphore + + allHosts[index].isUsed = Ping(allHosts[index].host) if isVerbose { - if hosts[index].isUsed { - fmt.Println("已使用IP:", hosts[index].host.String()) + if allHosts[index].isUsed { + fmt.Println("已使用IP:", allHosts[index].host.String()) } else { - fmt.Println("未使用IP:", hosts[index].host.String()) + fmt.Println("未使用IP:", allHosts[index].host.String()) } } }(index) } + checkerGroup.Wait() elapsed := time.Since(t) _, _ = fmt.Fprintln(os.Stderr, "IP扫描完成,耗时", elapsed) fmt.Println("已使用IP:") - printIPList(hosts, true) + printIPList(allHosts, true) fmt.Println("未使用IP:") - printIPList(hosts, false) + printIPList(allHosts, false) } +// printIPList prints the list of IPs based on the filter +// hosts: list of HostInfo to print +// boolFilter: if true, print used IPs; if false, print unused IPs func printIPList(hosts []HostInfo, boolFilter bool) { - position := 1 for _, hostInfo := range hosts { @@ -71,5 +96,12 @@ func printIPList(hosts []HostInfo, boolFilter bool) { position++ } } - fmt.Println() + + // Only add a newline if we've printed something and it didn't end with a newline + if position > 1 && (position-1)%OUTPUT_IP_PER_LINE != 0 { + fmt.Println() + } else if position == 1 { + // No IPs to print + fmt.Println("(无)") + } } diff --git a/pkg/lmap/common.go b/pkg/lmap/common.go index ec0b578..93119bd 100644 --- a/pkg/lmap/common.go +++ b/pkg/lmap/common.go @@ -20,7 +20,10 @@ package lmap import "net" +// HostInfo represents information about a host in the network type HostInfo struct { - host net.IP + // host is the IP address of the host + host net.IP + // isUsed indicates whether the host is active/responding isUsed bool } diff --git a/pkg/lmap/listen_icmp.go b/pkg/lmap/listen_icmp.go index 55d60ff..18d21e2 100644 --- a/pkg/lmap/listen_icmp.go +++ b/pkg/lmap/listen_icmp.go @@ -17,3 +17,45 @@ */ package lmap + +import ( + "fmt" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" +) + +// ListenICMP listens for ICMP packets on the specified address +// This function can be used to listen for ICMP replies when doing network scanning +func ListenICMP(addr string) error { + conn, err := icmp.ListenPacket("ip4:icmp", addr) + if err != nil { + return fmt.Errorf("failed to listen on ICMP: %w", err) + } + defer conn.Close() + + fmt.Printf("Listening for ICMP packets on %s...\n", addr) + + for { + buf := make([]byte, 1500) + n, peer, err := conn.ReadFrom(buf) + if err != nil { + return fmt.Errorf("failed to read ICMP packet: %w", err) + } + + msg, err := icmp.ParseMessage(1, buf[:n]) // 1 = ICMPv4 + if err != nil { + fmt.Printf("Failed to parse ICMP message from %s: %v\n", peer, err) + continue + } + + switch msg.Type { + case ipv4.ICMPTypeEchoReply: + fmt.Printf("Received ICMP Echo Reply from %s\n", peer) + case ipv4.ICMPTypeEcho: + fmt.Printf("Received ICMP Echo Request from %s\n", peer) + default: + fmt.Printf("Received ICMP message type %v from %s\n", msg.Type, peer) + } + } +} diff --git a/pkg/lmap/parse_subnet.go b/pkg/lmap/parse_subnet.go index ed49bc1..e89d35a 100644 --- a/pkg/lmap/parse_subnet.go +++ b/pkg/lmap/parse_subnet.go @@ -18,9 +18,12 @@ package lmap -import "net" +import ( + "net" + "strings" +) -func GetAllIPsFromCIDR(cidr string) ([]HostInfo, error) { +func GetAllIPsFromCIDR(cidr string, excludes []string) ([]HostInfo, error) { ip, ipNet, err := net.ParseCIDR(cidr) if err != nil { return nil, err @@ -28,15 +31,44 @@ func GetAllIPsFromCIDR(cidr string) ([]HostInfo, error) { var ips []HostInfo for ip := ip.Mask(ipNet.Mask); ipNet.Contains(ip); inc(ip) { - ips = append(ips, HostInfo{ - host: dupIP(ip), - isUsed: false, - }) + ipStr := ip.String() + if !isExcluded(ipStr, excludes) { + ips = append(ips, HostInfo{ + host: dupIP(ip), + isUsed: false, + }) + } + } + + // For larger networks, remove network and broadcast addresses + // For very small networks (like /31, /32), keep all addresses + ones, bits := ipNet.Mask.Size() + if bits-ones > 2 && len(ips) > 2 { + return ips[1 : len(ips)-1], nil // Remove network and broadcast addresses } - if len(ips) <= 2 { - return ips, nil + return ips, nil +} + +func isExcluded(ip string, excludes []string) bool { + for _, exclude := range excludes { + if strings.Contains(exclude, "/") { + _, excludeNet, err := net.ParseCIDR(exclude) + if err != nil { + //fmt.Printf("解析排除规则 %s 失败: %v\n", exclude, err) + continue + } + if excludeNet.Contains(net.ParseIP(ip)) { + //fmt.Printf("IP %s 被排除规则 %s 排除\n", ip, exclude) + return true + } + } else { + if ip == exclude { + //fmt.Printf("IP %s 被排除规则 %s 排除\n", ip, exclude) + return true + } + } } - return ips[0 : len(ips)-1], nil + return false } func inc(ip net.IP) { @@ -49,7 +81,6 @@ func inc(ip net.IP) { } func dupIP(ip net.IP) net.IP { - // To save space, try and only use 4 bytes if x := ip.To4(); x != nil { ip = x } diff --git a/pkg/lmap/ping.go b/pkg/lmap/ping.go index b09c070..314af3a 100644 --- a/pkg/lmap/ping.go +++ b/pkg/lmap/ping.go @@ -27,23 +27,27 @@ import ( "golang.org/x/net/ipv4" ) +// Ping sends an ICMP echo request to the given IP address and waits for a reply +// Returns true if a reply is received within the timeout, false otherwise func Ping(ip net.IP) bool { msg := icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, - Body: &icmp.RawBody{ - Data: ip, + Body: &icmp.Echo{ + ID: 12345, + Seq: 1, + Data: []byte("lmap ping"), }, } sendBytes, err := msg.Marshal(nil) if err != nil { log.Println("marshal icmp message", err) + return false } // Start listening for icmp replies conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") - if err != nil { log.Println("dial error:", err) return false @@ -54,19 +58,33 @@ func Ping(ip net.IP) bool { IP: ip, }) if err != nil { + log.Println("write error:", err) return false } + _ = conn.SetReadDeadline(time.Now().Add(time.Second * 2)) - for { - recvBuf := make([]byte, 20) - _, addr, err := conn.ReadFrom(recvBuf) + // Only read a limited number of times to avoid infinite loop + for i := 0; i < 5; i++ { + recvBuf := make([]byte, 1500) + n, addr, err := conn.ReadFrom(recvBuf) if err != nil { + // Timeout or other error, return false return false } if addr.String() == ip.String() { - return true + // Parse the received message to ensure it's an echo reply + reply, err := icmp.ParseMessage(1, recvBuf[:n]) + if err != nil { + continue + } + + if reply.Type == ipv4.ICMPTypeEchoReply { + return true + } } } + + return false } diff --git a/pkg/lmap/ping_test.go b/pkg/lmap/ping_test.go index 27c3548..2938332 100644 --- a/pkg/lmap/ping_test.go +++ b/pkg/lmap/ping_test.go @@ -29,3 +29,19 @@ func TestPing(t *testing.T) { res := Ping(ip) got.T(t).True(res) } + +func TestPingWithInvalidIP(t *testing.T) { + // 测试无效IP的情况 + invalidIP := net.ParseIP("0.0.0.0") + res := Ping(invalidIP) + // 对于无效IP,期望返回false + got.T(t).False(res) +} + +func TestPingWithLocalhost(t *testing.T) { + // 测试本地回环地址 + localhost := net.ParseIP("127.0.0.1") + res := Ping(localhost) + // 本地回环地址通常应该返回true + got.T(t).True(res) +}