正在加载端口信息...
1310 |├── config ├── hidden_ports.json └── config.json ├── requirements.txt ├── docker-compose.yml ├── Dockerfile ├── config.json.example ├── .github └── workflows │ └── docker-publish.yml ├── README.md ├── app.py └── templates └── index.html /config/hidden_ports.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | docker==6.1.3 3 | Werkzeug==2.3.7 4 | Jinja2==3.1.2 5 | MarkupSafe==2.1.3 6 | itsdangerous==2.1.2 7 | click==8.1.7 8 | blinker==1.6.3 9 | requests==2.31.0 10 | psutil==5.9.5 11 | urllib3==2.0.7 12 | certifi==2023.7.22 13 | charset-normalizer==3.3.0 14 | idna==3.4 15 | websocket-client==1.6.4 16 | packaging==23.2 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | dockports: 5 | image: crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports:latest # 国内镜像 6 | # image: ghcr.io/coracoo/dockports:latest # github镜像 7 | container_name: dockports 8 | network_mode: host 9 | volumes: 10 | - /var/run/docker.sock:/var/run/docker.sock:ro 11 | - ./config:/app/config 12 | environment: 13 | - DOCKPORTS_PORT=7577 # 可修改此端口以避免冲突 14 | # 如果需要使用命令行参数,可以取消注释下面的行 15 | # command: ["--port", "8080", "--debug"] 16 | restart: unless-stopped 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | # 设置工作目录 4 | WORKDIR /app 5 | 6 | # 设置环境变量 7 | ENV PYTHONUNBUFFERED=1 8 | ENV PYTHONDONTWRITEBYTECODE=1 9 | 10 | # 安装系统依赖、构建工具和Docker客户端 11 | RUN apt-get update && apt-get install -y \ 12 | net-tools \ 13 | procps \ 14 | curl \ 15 | ca-certificates \ 16 | gnupg \ 17 | lsb-release \ 18 | gcc \ 19 | python3-dev \ 20 | build-essential \ 21 | && mkdir -p /etc/apt/keyrings \ 22 | && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ 23 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ 24 | && apt-get update \ 25 | && apt-get install -y docker-ce-cli \ 26 | && rm -rf /var/lib/apt/lists/* 27 | 28 | # 复制依赖文件 29 | COPY requirements.txt . 30 | 31 | # 安装Python依赖,然后清理构建工具以减少镜像大小 32 | RUN pip install --no-cache-dir -r requirements.txt \ 33 | && apt-get remove -y gcc python3-dev build-essential \ 34 | && apt-get autoremove -y \ 35 | && apt-get clean \ 36 | && rm -rf /var/lib/apt/lists/* 37 | 38 | # 复制应用代码 39 | COPY . . 40 | 41 | # 设置默认端口环境变量 42 | ENV DOCKPORTS_PORT=7577 43 | 44 | # 暴露端口(默认7577,可通过环境变量或命令行参数修改) 45 | EXPOSE $DOCKPORTS_PORT 46 | 47 | # 健康检查(使用环境变量) 48 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 49 | CMD curl -f http://localhost:${DOCKPORTS_PORT}/ || exit 1 50 | 51 | # 启动应用(使用ENTRYPOINT支持命令行参数) 52 | ENTRYPOINT ["python", "app.py"] 53 | CMD [] -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "文件传输": 21, 3 | "远程登录": 22, 4 | "远程终端": 23, 5 | "邮件发送": 25, 6 | "域名解析": "53:udp", 7 | "DHCP服务器": "67:udp", 8 | "DHCP客户端": "68:udp", 9 | "简单文件传输": 69, 10 | "网页服务": 80, 11 | "身份认证": 88, 12 | "邮件接收": 110, 13 | "远程过程调用": 111, 14 | "网络时间": "123:udp", 15 | "RPC端点": 135, 16 | "NetBIOS名称": "137:udp", 17 | "NetBIOS数据报": "138:udp", 18 | "NetBIOS": 139, 19 | "邮件访问": 143, 20 | "网络管理": "161:udp", 21 | "SNMP陷阱": "162:udp", 22 | "目录访问": 389, 23 | "安全网页": 443, 24 | "文件共享": 445, 25 | "Kerberos密码": 464, 26 | "IPSec": "500:udp", 27 | "系统日志": "514:udp", 28 | "苹果文件": 548, 29 | "流媒体": 554, 30 | "网络打印": 631, 31 | "安全目录": 636, 32 | "Kerberos管理": 749, 33 | "Kerberos v4": 750, 34 | "安全邮件": 993, 35 | "安全POP3": 995, 36 | "SOCKS代理": 1080, 37 | "虚拟专网": "1194:udp", 38 | "SQL服务器": 1433, 39 | "Oracle数据库": 1521, 40 | "点对点隧道": 1723, 41 | "多媒体流": 1755, 42 | "RADIUS认证": "1812:udp", 43 | "RADIUS计费": "1813:udp", 44 | "MSN": 1863, 45 | "实时流媒体": 1935, 46 | "网络文件系统": 2049, 47 | "分布式协调": 2181, 48 | "Node.js开发": 3000, 49 | "React开发": 3001, 50 | "Squid代理": 3128, 51 | "MySQL数据库": 3306, 52 | "数据库": 3307, 53 | "远程桌面": 3389, 54 | "Angular开发": 4200, 55 | "RabbitMQ端口映射": 4369, 56 | "IPSec NAT穿透": "4500:udp", 57 | "Flask开发": 5000, 58 | "会话发起": 5060, 59 | "安全SIP": 5061, 60 | "即时通讯": 5222, 61 | "安全XMPP": 5223, 62 | "XMPP服务器": 5269, 63 | "多播DNS": "5353:udp", 64 | "PostgreSQL数据库": 5432, 65 | "日志分析": 5601, 66 | "消息队列": 5672, 67 | "远程控制": 5900, 68 | "文档数据库": 5984, 69 | "X11图形": 6000, 70 | "Redis缓存": 6379, 71 | "IRC聊天": 6667, 72 | "安全IRC": 6697, 73 | "Cassandra数据库": 7000, 74 | "安全Cassandra": 7001, 75 | "Django开发": 8000, 76 | "备用网页": 8080, 77 | "模式注册": 8081, 78 | "Kafka REST": 8082, 79 | "Kafka连接": 8083, 80 | "时序数据库": 8086, 81 | "隐私代理": 8118, 82 | "备用安全网页": 8443, 83 | "Jupyter笔记本": 8888, 84 | "搜索引擎": 8983, 85 | "代码质量": 9000, 86 | "Cassandra查询": 9042, 87 | "Tor SOCKS": 9050, 88 | "Tor控制": 9051, 89 | "备用网页2": 9080, 90 | "监控系统": 9090, 91 | "消息流": 9092, 92 | "Cassandra Thrift": 9160, 93 | "搜索分析": 9200, 94 | "备用安全网页2": 9443, 95 | "Web管理": 10000, 96 | "内存缓存": 11211, 97 | "RabbitMQ管理": 15672, 98 | "RabbitMQ集群": 25672, 99 | "MongoDB数据库": 27017 100 | } -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker Multi-Platform Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | workflow_dispatch: 8 | 9 | # 添加权限配置 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | env: 15 | IMAGE_NAME: dockports 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v3 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | with: 30 | buildkitd-flags: --debug 31 | 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Login to Ali Registry 40 | uses: docker/login-action@v3 41 | with: 42 | registry: ${{ secrets.ALI_REGISTRY }} 43 | username: ${{ secrets.ALI_USERNAME }} 44 | password: ${{ secrets.ALI_PASSWORD }} 45 | 46 | - name: Extract metadata 47 | id: meta 48 | uses: docker/metadata-action@v5 49 | with: 50 | images: | 51 | ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} 52 | ${{ secrets.ALI_REGISTRY }}/cherry4nas/${{ env.IMAGE_NAME }} 53 | tags: | 54 | type=semver,pattern={{version}} 55 | type=semver,pattern={{major}}.{{minor}} 56 | type=semver,pattern={{major}} 57 | type=raw,value=latest,enable={{is_default_branch}} 58 | labels: | 59 | maintainer="可爱的小cherry" 60 | org.opencontainers.image.title="DockPorts" 61 | org.opencontainers.image.description="容器端口监控和可视化工具" 62 | org.opencontainers.image.version={{version}} 63 | org.opencontainers.image.revision={{sha}} 64 | 65 | - name: Build and push 66 | uses: docker/build-push-action@v5 67 | with: 68 | context: . 69 | file: ./Dockerfile 70 | platforms: linux/amd64,linux/arm64,linux/arm/v7 71 | push: true 72 | tags: ${{ steps.meta.outputs.tags }} 73 | labels: ${{ steps.meta.outputs.labels }} 74 | cache-from: type=gha 75 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Angular开发:host": "4200:tcp", 3 | "Cassandra Thrift:host": "9160:tcp", 4 | "Cassandra数据库:host": "7000:tcp", 5 | "Cassandra查询:host": "9042:tcp", 6 | "DHCP客户端:host": "68:udp", 7 | "DHCP服务器:host": "67:udp", 8 | "Django开发:host": "8000:tcp", 9 | "DockPorts:docker": "7575:tcp", 10 | "Flask开发:host": "5000:tcp", 11 | "IPSec NAT穿透:host": "4500:udp", 12 | "IPSec:host": "500:udp", 13 | "IRC聊天:host": "6667:tcp", 14 | "Jupyter笔记本:host": "8888:tcp", 15 | "Kafka REST:host": "8082:tcp", 16 | "Kafka连接:host": "8083:tcp", 17 | "Kerberos v4:host": "750:tcp", 18 | "Kerberos密码:host": "464:tcp", 19 | "Kerberos管理:host": "749:tcp", 20 | "MSN:host": "1863:tcp", 21 | "MongoDB数据库:host": "27017:tcp", 22 | "MySQL数据库:host": "3306:tcp", 23 | "NetBIOS:host": "139:tcp", 24 | "NetBIOS名称:host": "137:udp", 25 | "NetBIOS数据报:host": "138:udp", 26 | "Node.js开发:host": "3000:tcp", 27 | "Oracle数据库:host": "1521:tcp", 28 | "PostgreSQL数据库:host": "5432:tcp", 29 | "RADIUS计费:host": "1813:udp", 30 | "RADIUS认证:host": "1812:udp", 31 | "RPC端点:host": "135:tcp", 32 | "RabbitMQ端口映射:host": "4369:tcp", 33 | "RabbitMQ管理:host": "15672:tcp", 34 | "RabbitMQ集群:host": "25672:tcp", 35 | "React开发:host": "3001:tcp", 36 | "Redis缓存:host": "6379:tcp", 37 | "SNMP陷阱:host": "162:udp", 38 | "SOCKS代理:host": "1080:tcp", 39 | "SQL服务器:host": "1433:tcp", 40 | "Squid代理:host": "3128:tcp", 41 | "Tor SOCKS:host": "9050:tcp", 42 | "Tor控制:host": "9051:tcp", 43 | "Web管理:host": "10000:tcp", 44 | "X11图形:host": "6000:tcp", 45 | "XMPP服务器:host": "5269:tcp", 46 | "代码质量:host": "9000:tcp", 47 | "会话发起:host": "5060:tcp", 48 | "内存缓存:host": "11211:tcp", 49 | "分布式协调:host": "2181:tcp", 50 | "即时通讯:host": "5222:tcp", 51 | "域名解析:host": "53:udp", 52 | "备用安全网页2:host": "9443:tcp", 53 | "备用安全网页:host": "8443:tcp", 54 | "备用网页2:host": "9080:tcp", 55 | "备用网页:host": "8080:tcp", 56 | "多媒体流:host": "1755:tcp", 57 | "多播DNS:host": "5353:udp", 58 | "安全Cassandra:host": "7001:tcp", 59 | "安全IRC:host": "6697:tcp", 60 | "安全POP3:host": "995:tcp", 61 | "安全SIP:host": "5061:tcp", 62 | "安全XMPP:host": "5223:tcp", 63 | "安全目录:host": "636:tcp", 64 | "安全网页:host": "443:tcp", 65 | "安全邮件:host": "993:tcp", 66 | "实时流媒体:host": "1935:tcp", 67 | "搜索分析:host": "9200:tcp", 68 | "搜索引擎:host": "8983:tcp", 69 | "数据库:host": "3307:tcp", 70 | "文件传输:host": "21:tcp", 71 | "文件共享:host": "445:tcp", 72 | "文档数据库:host": "5984:tcp", 73 | "日志分析:host": "5601:tcp", 74 | "时序数据库:host": "8086:tcp", 75 | "模式注册:host": "8081:tcp", 76 | "流媒体:host": "554:tcp", 77 | "消息流:host": "9092:tcp", 78 | "消息队列:host": "5672:tcp", 79 | "点对点隧道:host": "1723:tcp", 80 | "监控系统:host": "9090:tcp", 81 | "目录访问:host": "389:tcp", 82 | "简单文件传输:host": "69:tcp", 83 | "系统日志:host": "514:udp", 84 | "网络打印:host": "631:tcp", 85 | "网络文件系统:host": "2049:tcp", 86 | "网络时间:host": "123:udp", 87 | "网络管理:host": "161:udp", 88 | "网页服务:host": "80:tcp", 89 | "苹果文件:host": "548:tcp", 90 | "虚拟专网:host": "1194:udp", 91 | "身份认证:host": "88:tcp", 92 | "远程控制:host": "5900:tcp", 93 | "远程桌面:host": "3389:tcp", 94 | "远程登录:host": "22:tcp", 95 | "远程终端:host": "23:tcp", 96 | "远程过程调用:host": "111:tcp", 97 | "邮件发送:host": "25:tcp", 98 | "邮件接收:host": "110:tcp", 99 | "邮件访问:host": "143:tcp", 100 | "隐私代理:host": "8118:tcp", 101 | "dockports:docker": "7577:tcp", 102 | "caddy:docker": "2019:tcp" 103 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 我的仓库 2 | 3 | **1️⃣** : 中文docker项目集成项目: [https://github.com/coracoo/awesome_docker_cn](https://github.com/coracoo/awesome_docker_cn) 4 | 5 | **2️⃣** : docker转compose:[https://github.com/coracoo/docker2compose](https://github.com/coracoo/docker2compose) 6 | 7 | **3️⃣** : 容器部署iSCSI,支持绿联极空间飞牛:[https://github.com/coracoo/d-tgtadm/](https://github.com/coracoo/d-tgtadm/) 8 | 9 | **4️⃣** : 容器端口检查工具: [https://github.com/coracoo/DockPorts/](https://github.com/coracoo/DockPorts) 10 | 11 | # 我的频道 12 | 13 | ### 首发平台——什么值得买: 14 | 15 | ### [⭐点我关注](https://zhiyou.smzdm.com/member/9674309982/) 16 | 17 | ### 微信公众号: 18 | 19 |  20 | 21 | --- 22 | 23 | # DockPorts - 容器端口监控工具 24 | 25 | 一个现代化的Docker容器端口监控和可视化工具,帮助您轻松管理和监控NAS或服务器上的端口使用情况。 26 | 27 | ## ✨ 功能特性 28 | 29 | - 🐳 **Docker集成**: 通过Docker API实时监控容器端口映射 30 | - 🖥️ **系统监控**: 使用netstat监控主机端口使用情况 31 | - 📊 **可视化展示**: 美观的卡片式界面,类似Docker Compose Maker风格 32 | - 🔄 **实时刷新**: 支持手动和自动刷新端口信息 33 | - 📱 **响应式设计**: 支持桌面和移动设备 34 | - 🎯 **智能排序**: 端口按顺序排列,空隙用灰色卡片标注 35 | - 🏷️ **来源标识**: 区分Docker容器端口和系统服务端口 36 | - 👁️ **端口隐藏**: 支持隐藏不需要显示的端口,提供"已隐藏"标签页查看 37 | - 📋 **批量操作**: 支持批量隐藏/取消隐藏端口范围 38 | - 🎨 **虚拟端口**: 隐藏端口以虚线边框样式区分显示 39 | - ⚡ **实时同步**: 隐藏/取消隐藏操作后立即更新显示状态 40 | 41 | ## 🖼️ 界面预览 42 | 43 | 界面采用现代化设计,包含: 44 | - 蓝色渐变背景 45 | - 卡片式端口展示 46 | - 实时统计信息 47 | - 响应式布局 48 | 49 | ## 🚀 快速开始 50 | 51 | ### 📦 镜像获取 52 | 53 | DockPorts 提供多个镜像源,支持多平台架构: 54 | 55 | | 镜像源 | 镜像地址 | 支持架构 | 说明 | 56 | |--------|----------|----------|------| 57 | | **GitHub Container Registry** | `ghcr.io/coracoo/dockports:latest` | `amd64`, `arm64`, `arm/v7` | 官方推荐,全球访问 | 58 | | **阿里云容器镜像服务** | `crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports:latest` | `amd64`, `arm64`, `arm/v7` | 国内用户推荐,访问更快 | 59 | 60 | **支持的平台:** 61 | - `linux/amd64` - x86_64 架构(Intel/AMD 处理器) 62 | - `linux/arm64` - ARM64 架构(Apple M1/M2、树莓派4等) 63 | - `linux/arm/v7` - ARMv7 架构(树莓派3等) 64 | 65 | **版本标签说明:** 66 | - `latest` - 最新稳定版本(推荐生产环境使用) 67 | - `v0.2.0` - 具体版本号(推荐锁定版本使用) 68 | - `v0.2` - 主要版本号(自动获取最新的0.2.x版本) 69 | - `v0` - 大版本号(自动获取最新的0.x.x版本) 70 | 71 | ### 使用Docker Compose(推荐) 72 | 73 | 1. 克隆项目: 74 | ```bash 75 | git clone https://github.com/coracoo/DockPorts.git 76 | cd DockPorts 77 | ``` 78 | 79 | 2. **选择镜像源**(可选): 80 | 81 | 默认使用 GitHub 镜像,如需使用阿里云镜像,请修改 `docker-compose.yml`: 82 | ```yaml 83 | services: 84 | dockports: 85 | # 将下面这行: 86 | # image: ghcr.io/coracoo/dockports:latest 87 | # 替换为: 88 | image: crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports:latest 89 | ``` 90 | 91 | 3. 启动服务: 92 | ```bash 93 | docker-compose up -d 94 | ``` 95 | 96 | 4. 访问应用: 97 | 打开浏览器访问 `http://localhost:7577` 98 | 99 | ### 使用Docker运行 100 | 101 | #### 使用GitHub镜像(国外用户推荐) 102 | 103 | ```bash 104 | # 使用默认端口7577 105 | docker run -d \ 106 | --name dockports \ 107 | --network host \ 108 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 109 | -v ./config:/app/config \ 110 | ghcr.io/coracoo/dockports:latest 111 | 112 | # 使用自定义端口8080 113 | docker run -d \ 114 | --name dockports \ 115 | --network host \ 116 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 117 | -v ./config:/app/config \ 118 | -e DOCKPORTS_PORT=8080 \ 119 | ghcr.io/coracoo/dockports:latest 120 | 121 | # 启用调试模式 122 | docker run -d \ 123 | --name dockports \ 124 | --network host \ 125 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 126 | -v ./config:/app/config \ 127 | ghcr.io/coracoo/dockports:latest --debug 128 | ``` 129 | 130 | #### 使用阿里云镜像(国内用户推荐) 131 | 132 | ```bash 133 | # 使用默认端口7577 134 | docker run -d \ 135 | --name dockports \ 136 | --network host \ 137 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 138 | -v ./config:/app/config \ 139 | crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports:latest 140 | 141 | # 使用自定义端口8080 142 | docker run -d \ 143 | --name dockports \ 144 | --network host \ 145 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 146 | -v ./config:/app/config \ 147 | -e DOCKPORTS_PORT=8080 \ 148 | crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports:latest 149 | 150 | # 启用调试模式 151 | docker run -d \ 152 | --name dockports \ 153 | --network host \ 154 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 155 | -v ./config:/app/config \ 156 | crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports:latest --debug 157 | ``` 158 | 159 | ### 本地开发 160 | 161 | 1. 安装依赖: 162 | ```bash 163 | pip install -r requirements.txt 164 | ``` 165 | 166 | 2. 运行应用: 167 | ```bash 168 | # 使用默认端口7577 169 | python app.py 170 | 171 | # 使用自定义端口 172 | python app.py --port 8080 173 | 174 | # 启用调试模式 175 | python app.py --debug 176 | 177 | # 查看帮助信息 178 | python app.py --help 179 | ``` 180 | 181 | ## 📋 系统要求 182 | 183 | - Docker Engine 20.10+ 184 | - Docker Compose 2.0+ 185 | - Linux系统(支持netstat命令) 186 | - 端口7577可用 187 | 188 | ## 🔧 技术架构 189 | 190 | - **后端**: Python Flask + Docker API + psutil 191 | - **前端**: HTML + CSS + JavaScript (原生) 192 | - **容器化**: Docker + Docker Compose 193 | - **CI/CD**: GitHub Actions + 多平台构建 194 | - **镜像分发**: GitHub Container Registry + 阿里云容器镜像服务 195 | 196 | ### 🏗️ CI/CD 流程 197 | 198 | 项目采用 GitHub Actions 实现自动化构建和发布: 199 | 200 | 1. **触发条件**: 201 | - 推送版本标签(`v*.*.*`格式) 202 | - 手动触发工作流 203 | 204 | 2. **多平台构建**: 205 | - 使用 Docker Buildx 构建多架构镜像 206 | - 支持 `linux/amd64`、`linux/arm64`、`linux/arm/v7` 207 | - 利用 QEMU 实现跨平台编译 208 | 209 | 3. **镜像发布**: 210 | - **GitHub Container Registry**: `ghcr.io/coracoo/dockports` 211 | - **阿里云容器镜像服务**: `crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports` 212 | 213 | 4. **版本管理**: 214 | - 自动提取语义化版本号(如 v1.2.3) 215 | - 生成多个版本标签:`v1.2.3`、`v1.2`、`v1`、`latest` 216 | - 包含完整的镜像元数据和标签 217 | - 支持版本回滚和多版本并存 218 | 219 | ### Host网络容器端口检测机制 220 | 221 | 对于使用host网络模式的Docker容器,DockPorts采用多维度智能检测机制: 222 | 223 | 1. **ExposedPorts配置检测** 224 | - 从容器的`Config.ExposedPorts`中获取声明的端口 225 | - 解析端口格式(如`80/tcp`、`53/udp`) 226 | 227 | 2. **Healthcheck健康检查检测** 228 | - 解析`Config.Healthcheck.Test`命令 229 | - 使用正则表达式匹配`localhost:port`、`127.0.0.1:port`等模式 230 | - 自动提取健康检查中使用的端口号 231 | 232 | 3. **Entrypoint和Cmd命令检测** 233 | - 分析容器启动命令和入口点 234 | - 支持多种端口参数格式: 235 | - `--port=8080`、`--port 8080` 236 | - `-p=8080`、`-p 8080` 237 | - `--listen=8080`、`--bind=0.0.0.0:8080` 238 | - 通用`:port`模式 239 | 240 | 4. **环境变量检测** 241 | - 扫描容器环境变量中的端口配置 242 | - 识别`PORT`、`HTTP_PORT`、`LISTEN_PORT`等常见变量 243 | - 从变量值中提取端口号 244 | 245 | 5. **智能端口合并** 246 | - 将所有检测到的端口合并到`exposed_ports`集合 247 | - 提供详细的端口来源分类(健康检查、入口点、环境变量等) 248 | - 避免重复端口,确保数据准确性 249 | 250 | ## 🔧 配置说明 251 | 252 | ### 环境变量 253 | 254 | | 变量名 | 默认值 | 说明 | 255 | |--------|--------|------| 256 | | `TZ` | `Asia/Shanghai` | 时区设置 | 257 | | `PYTHONUNBUFFERED` | `1` | Python输出缓冲设置 | 258 | 259 | ## 🔧 配置说明 260 | 261 | ### 命令行参数 262 | 263 | DockPorts 支持以下命令行参数来自定义运行配置: 264 | 265 | | 参数 | 简写 | 默认值 | 说明 | 266 | |------|------|--------|------| 267 | | `--port` | `-p` | 7577 | Web服务端口 | 268 | | `--host` | - | 0.0.0.0 | Web服务监听地址 | 269 | | `--debug` | - | false | 启用调试模式 | 270 | | `--help` | `-h` | - | 显示帮助信息 | 271 | 272 | **使用示例:** 273 | ```bash 274 | # 修改端口以避免冲突 275 | python app.py --port 8080 276 | 277 | # 在Docker中使用自定义端口 278 | docker run -d --name dockports --network host \ 279 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 280 | -v ./config:/app/config \ 281 | dockports --port 8080 282 | 283 | # 在docker-compose中使用命令行参数 284 | # 取消注释 docker-compose.yml 中的 command 行并修改参数 285 | ``` 286 | 287 | ### 环境变量支持 288 | 289 | 除了命令行参数外,DockPorts还支持通过环境变量进行配置: 290 | 291 | | 环境变量 | 对应参数 | 默认值 | 说明 | 292 | |----------|----------|--------|------| 293 | | `DOCKPORTS_PORT` | `--port` | 7577 | Web服务端口 | 294 | | `DOCKPORTS_HOST` | `--host` | 0.0.0.0 | Web服务监听地址 | 295 | | `DOCKPORTS_DEBUG` | `--debug` | false | 启用调试模式(设置为true、1或yes) | 296 | 297 | **配置优先级:** 命令行参数 > 环境变量 > 默认值 298 | 299 | **使用示例:** 300 | ```bash 301 | # 使用环境变量设置端口 302 | export DOCKPORTS_PORT=8080 303 | python app.py 304 | 305 | # 使用环境变量启用调试模式 306 | export DOCKPORTS_DEBUG=true 307 | python app.py 308 | 309 | # Docker容器中使用环境变量 310 | docker run -d --name dockports --network host \ 311 | -e DOCKPORTS_PORT=8080 \ 312 | -e DOCKPORTS_DEBUG=true \ 313 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 314 | -v ./config:/app/config \ 315 | dockports 316 | 317 | # docker-compose中使用环境变量 318 | # 在docker-compose.yml的environment部分添加: 319 | # environment: 320 | # - DOCKPORTS_PORT=8080 321 | # - DOCKPORTS_DEBUG=true 322 | ``` 323 | 324 | ### 卷映射 325 | 326 | | 主机路径 | 容器路径 | 说明 | 327 | |----------|----------|------| 328 | | `/var/run/docker.sock` | `/var/run/docker.sock` | Docker API访问(只读) | 329 | | `./config` | `/app/config` | 配置文件目录 | 330 | 331 | ## 📊 功能详解 332 | 333 | ### 端口监控 334 | 335 | 1. **Docker容器端口**: 336 | - 自动发现所有运行中的容器 337 | - 获取端口映射信息 338 | - 识别host网络模式容器 339 | 340 | 2. **系统端口**: 341 | - 使用netstat扫描监听端口 342 | - 支持TCP和UDP协议 343 | - 识别系统服务占用的端口 344 | 345 | ### 可视化展示 346 | 347 | 1. **端口卡片**: 348 | - 大号端口号显示 349 | - 容器名称和内部端口 350 | - 来源标识(Docker/系统) 351 | - 隐藏/取消隐藏按钮 352 | 353 | 2. **间隔卡片**: 354 | - 显示连续端口间的空隙 355 | - 标注可用端口数量 356 | - 灰色样式区分 357 | - 支持批量隐藏端口范围 358 | 359 | 3. **虚拟端口卡片**: 360 | - 虚线边框样式区分 361 | - 显示"已隐藏端口"标识 362 | - 支持取消隐藏操作 363 | 364 | 4. **标签页导航**: 365 | - 全部端口 366 | - 已使用端口 367 | - 可用端口 368 | - 已隐藏端口 369 | 370 | 5. **统计信息**: 371 | - 已使用端口总数 372 | - 可用端口总数 373 | - Docker容器数量 374 | 375 | ## 🛠️ API接口 376 | 377 | ### GET /api/ports 378 | 获取端口信息 379 | 380 | **响应示例**: 381 | ```json 382 | { 383 | "success": true, 384 | "data": { 385 | "port_cards": [ 386 | { 387 | "port": 80, 388 | "type": "used", 389 | "container_name": "nginx", 390 | "container_port": "80/tcp", 391 | "source": "docker" 392 | } 393 | ], 394 | "total_used": 10, 395 | "total_available": 65525, 396 | "docker_containers": 5 397 | } 398 | } 399 | ``` 400 | 401 | ### GET /api/refresh 402 | 刷新端口信息 403 | 404 | ### 隐藏端口管理 405 | 406 | #### GET /api/hidden-ports 407 | 获取隐藏端口列表 408 | 409 | #### POST /api/hidden-ports 410 | 隐藏单个端口 411 | ```json 412 | { 413 | "port": 8080 414 | } 415 | ``` 416 | 417 | #### DELETE /api/hidden-ports 418 | 取消隐藏单个端口 419 | ```json 420 | { 421 | "port": 8080 422 | } 423 | ``` 424 | 425 | #### POST /api/hidden-ports/batch 426 | 批量隐藏端口 427 | ```json 428 | { 429 | "ports": [8080, 8081, 8082] 430 | } 431 | ``` 432 | 433 | #### DELETE /api/hidden-ports/batch 434 | 批量取消隐藏端口 435 | ```json 436 | { 437 | "ports": [8080, 8081, 8082] 438 | } 439 | ``` 440 | 441 | ## 🔍 故障排除 442 | 443 | ### 常见问题 444 | 445 | 1. **镜像拉取失败**: 446 | - **阿里云镜像拉取慢**:尝试使用阿里云镜像 447 | ```bash 448 | docker pull crpi-xg6dfmt5h2etc7hg.cn-hangzhou.personal.cr.aliyuncs.com/cherry4nas/dockports:latest 449 | ``` 450 | - **阿里云镜像访问失败**:切换回GitHub镜像 451 | ```bash 452 | docker pull ghcr.io/coracoo/dockports:latest 453 | ``` 454 | - **架构不匹配**:确认您的设备架构,镜像支持 `amd64`、`arm64`、`arm/v7` 455 | 456 | 2. **无法连接Docker**: 457 | - 确保Docker socket已正确映射 458 | - 检查容器是否有访问Docker的权限 459 | 460 | 3. **netstat命令不可用**: 461 | - 确保容器内安装了net-tools包 462 | - 检查/proc目录是否正确映射 463 | 464 | 4. **端口7577被占用**: 465 | - 修改docker-compose.yml中的端口映射 466 | - 或停止占用该端口的服务 467 | - 使用环境变量自定义端口:`-e DOCKPORTS_PORT=8080` 468 | 469 | 5. **隐藏端口功能异常**: 470 | - 检查config/hidden_ports.json文件权限 471 | - 确保容器有写入配置文件的权限 472 | - 查看浏览器控制台是否有JavaScript错误 473 | 474 | 6. **取消隐藏端口范围失败**: 475 | - 确保使用最新版本,包含批量取消隐藏API 476 | - 检查网络连接和API响应 477 | 478 | 7. **多架构部署问题**: 479 | - 树莓派等ARM设备确保使用正确的架构标签 480 | - 如遇到架构问题,可以手动指定平台: 481 | ```bash 482 | docker run --platform linux/arm64 ... 483 | ``` 484 | 485 | ### 日志查看 486 | 487 | ```bash 488 | # 查看容器日志 489 | docker-compose logs -f dockports 490 | 491 | # 查看实时日志 492 | docker logs -f dockports 493 | ``` 494 | 495 | ## 🤝 贡献指南 496 | 497 | 欢迎提交Issue和Pull Request! 498 | 499 | 1. Fork项目 500 | 2. 创建功能分支 501 | 3. 提交更改 502 | 4. 推送到分支 503 | 5. 创建Pull Request 504 | 505 | ## 📝 更新日志 506 | 507 | ### v0.2.1 (最新) 508 | - 🔧 修复`config.json`配置文件的读取、保存、结构 509 | - ⚙️ `config.json`修改为`"服务名:docker/host":"端口:/tcp/udp"`的格式 510 | - 📊 添加端口名称时,现在可以选择是`宿主机服务`,还是`Docker服务` 511 | 512 | ### v0.2.0 513 | - 🌐 **多平台支持**:新增 ARM64 和 ARMv7 架构支持 514 | - 🚀 **阿里云镜像**:发布阿里云容器镜像服务,国内用户访问更快 515 | - 🏗️ **CI/CD优化**:GitHub Actions 自动化多平台构建和发布 516 | - 🔧 **端口范围锁定**:新增端口范围锁定功能,支持指定范围查看 517 | - 📊 **前端优化**:修复端口范围锁定后的数据刷新问题 518 | - 🐳 **镜像分发**:同时发布到 GitHub Container Registry 和阿里云 519 | - 📖 **文档完善**:更新README,添加多镜像源使用说明 520 | - 🔌 **协议区分**:支持UDP/TCP协议过滤和统计,提供协议切换按钮 521 | 522 | ### v0.1.2 523 | - 🔧 修复自定义端口启动失败问题 524 | - 📊 增强启动日志,显示实际使用的配置参数 525 | 526 | ### v0.1.1 527 | - 🔧 增加host网络模式的处理 528 | - 🐳 优化Docker容器端口检测机制 529 | 530 | ### v0.1.0 531 | - ⚙️ 新增命令行参数支持(--port, --host, --debug) 532 | - 🔧 支持自定义Web服务端口,解决host网络模式下端口冲突问题 533 | - 🐳 优化Docker镜像构建,支持ENTRYPOINT传参 534 | - 📦 更新GitHub Actions配置,统一镜像名为DockPorts 535 | - 👁️ 端口隐藏功能,支持隐藏不需要显示的端口 536 | - 🐳 基础Docker容器端口监控功能 537 | - 🖥️ 系统端口监控功能 538 | - 📊 可视化端口展示界面 539 | - 🔄 实时刷新功能 540 | 541 | ## 📄 许可证 542 | 543 | MIT License 544 | 545 | ## 🙏 致谢 546 | 547 | - 界面设计灵感来源于 [Docker Compose Maker](https://github.com/ajnart/dcm) 548 | - 感谢Docker和Flask社区的支持 549 | 550 | ## 📞 联系方式 551 | 552 | 如有问题或建议,请通过以下方式联系: 553 | - 提交GitHub Issue 554 | - 发送邮件至项目维护者 555 | 556 | --- 557 | 558 | **DockPorts** - 让端口管理变得简单高效! 🚀 559 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | DockPorts - 容器化NAS端口记录工具 5 | 主要功能: 6 | 1. 通过Docker API监控容器端口映射 7 | 2. 通过netstat监控主机端口使用情况 8 | 3. 可视化展示端口使用状态 9 | """ 10 | 11 | import docker 12 | import subprocess 13 | import json 14 | import re 15 | from flask import Flask, render_template, jsonify, request 16 | from collections import defaultdict 17 | import logging 18 | from datetime import datetime, timedelta 19 | import os 20 | import socket 21 | import time 22 | from functools import lru_cache 23 | import argparse 24 | 25 | # 配置日志 26 | logging.basicConfig( 27 | level=logging.INFO, 28 | format='%(levelname)s:%(name)s:%(message)s' 29 | ) 30 | logger = logging.getLogger(__name__) 31 | 32 | app = Flask(__name__) 33 | 34 | # 配置文件路径 35 | CONFIG_DIR = '/app/config' 36 | CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.json') 37 | HIDDEN_PORTS_FILE = os.path.join(CONFIG_DIR, 'hidden_ports.json') 38 | DEFAULT_CONFIG_FILE = '/app/config/config.json' 39 | 40 | def init_config(): 41 | """初始化配置文件""" 42 | import shutil 43 | 44 | # 确保配置目录存在 45 | os.makedirs(CONFIG_DIR, exist_ok=True) 46 | 47 | # 初始化主配置文件 48 | config_created = False 49 | if not os.path.exists(CONFIG_FILE): 50 | # 配置文件不存在时,从示例文件复制 51 | example_config_file = os.path.join(os.path.dirname(__file__), 'config.json.example') 52 | 53 | if os.path.exists(example_config_file): 54 | # 从示例文件复制配置 55 | shutil.copy2(example_config_file, CONFIG_FILE) 56 | print(f"配置文件已从示例文件复制: {CONFIG_FILE}") 57 | else: 58 | # 如果示例文件不存在,创建默认配置(向后兼容) 59 | default_config = { 60 | "远程登录:host": "22:tcp", 61 | "HTTP:host": "80:tcp", 62 | "HTTPS:host": "443:tcp", 63 | "MySQL数据库:host": "3306:tcp", 64 | "PostgreSQL数据库:host": "5432:tcp", 65 | "Redis缓存:host": "6379:tcp", 66 | "MongoDB数据库:host": "27017:tcp", 67 | "搜索分析:host": "9200:tcp", 68 | "DockPorts:docker": "7575:tcp" 69 | } 70 | 71 | with open(CONFIG_FILE, 'w', encoding='utf-8') as f: 72 | json.dump(default_config, f, indent=2, ensure_ascii=False) 73 | 74 | print(f"配置文件已创建(默认配置): {CONFIG_FILE}") 75 | config_created = True 76 | else: 77 | print(f"配置文件已存在: {CONFIG_FILE}") 78 | 79 | # 初始化隐藏端口配置文件 80 | if not os.path.exists(HIDDEN_PORTS_FILE): 81 | # 创建空的隐藏端口配置文件 82 | with open(HIDDEN_PORTS_FILE, 'w', encoding='utf-8') as f: 83 | json.dump([], f, indent=2, ensure_ascii=False) 84 | print(f"隐藏端口配置文件已创建: {HIDDEN_PORTS_FILE}") 85 | else: 86 | print(f"隐藏端口配置文件已存在: {HIDDEN_PORTS_FILE}") 87 | 88 | def load_config(): 89 | """加载配置文件,支持新格式:服务名:docker/host -> 端口:tcp/udp""" 90 | try: 91 | with open(CONFIG_FILE, 'r', encoding='utf-8') as f: 92 | raw_config = json.load(f) 93 | 94 | # 处理配置文件,支持新格式 95 | processed_config = {} 96 | for key, value in raw_config.items(): 97 | if isinstance(value, str) and ':' in value: 98 | # 检查是否为新格式:服务名:docker/host -> 端口:tcp/udp 99 | if ':' in key and (key.endswith(':docker') or key.endswith(':host')): 100 | # 新格式的键:服务名:docker/host 101 | service_name = key.rsplit(':', 1)[0] # 提取服务名 102 | service_type = key.rsplit(':', 1)[1] # 提取服务类型 103 | 104 | # 解析值:端口:协议 105 | value_parts = value.split(':') 106 | if len(value_parts) >= 2: 107 | try: 108 | port = int(value_parts[0]) 109 | protocol = value_parts[1].upper() 110 | processed_config[service_name] = { 111 | 'port': port, 112 | 'protocol': protocol, 113 | 'service_type': service_type 114 | } 115 | except ValueError: 116 | processed_config[key] = value 117 | else: 118 | processed_config[key] = value 119 | else: 120 | # 兼容旧格式:"服务名": "端口:协议" 121 | parts = value.split(':') 122 | if len(parts) >= 2: 123 | try: 124 | port = int(parts[0]) # 第一部分是端口号 125 | protocol = parts[1].upper() if parts[1].upper() in ['TCP', 'UDP'] else 'TCP' 126 | processed_config[key] = {'port': port, 'protocol': protocol} 127 | except ValueError: 128 | processed_config[key] = value 129 | else: 130 | processed_config[key] = value 131 | elif isinstance(value, int): 132 | # 默认为TCP协议 133 | processed_config[key] = {'port': value, 'protocol': 'TCP'} 134 | else: 135 | processed_config[key] = value 136 | 137 | return processed_config 138 | except Exception as e: 139 | print(f"加载配置文件失败: {e}") 140 | # 返回默认配置 141 | return { 142 | "ssh": {'port': 22, 'protocol': 'TCP'}, 143 | "http": {'port': 80, 'protocol': 'TCP'}, 144 | "https": {'port': 443, 'protocol': 'TCP'}, 145 | "mysql": {'port': 3306, 'protocol': 'TCP'}, 146 | "postgresql": {'port': 5432, 'protocol': 'TCP'}, 147 | "redis": {'port': 6379, 'protocol': 'TCP'}, 148 | "mongodb": {'port': 27017, 'protocol': 'TCP'}, 149 | "elasticsearch": {'port': 9200, 'protocol': 'TCP'}, 150 | "app_settings": { 151 | "host": "0.0.0.0", 152 | "port": 7577, 153 | "debug": False 154 | } 155 | } 156 | 157 | def save_config(config): 158 | """保存配置文件,使用新格式:服务名:docker/host -> 端口:tcp/udp""" 159 | try: 160 | # 处理配置文件,将协议信息转换为新的字符串格式 161 | raw_config = {} 162 | for key, value in config.items(): 163 | if isinstance(value, dict) and 'port' in value and 'protocol' in value: 164 | port = value['port'] 165 | protocol = value['protocol'].lower() 166 | # 确定服务类型(默认为host,如果有容器信息则为docker) 167 | service_type = value.get('service_type', 'host') 168 | # 新格式:"服务名:docker/host":"端口:tcp/udp" 169 | new_key = f"{key}:{service_type}" 170 | raw_config[new_key] = f"{port}:{protocol}" 171 | else: 172 | raw_config[key] = value 173 | 174 | with open(CONFIG_FILE, 'w', encoding='utf-8') as f: 175 | json.dump(raw_config, f, indent=2, ensure_ascii=False) 176 | return True 177 | except Exception as e: 178 | print(f"保存配置文件失败: {e}") 179 | return False 180 | 181 | def load_hidden_ports(): 182 | """加载隐藏端口配置""" 183 | try: 184 | if os.path.exists(HIDDEN_PORTS_FILE): 185 | with open(HIDDEN_PORTS_FILE, 'r', encoding='utf-8') as f: 186 | return json.load(f) 187 | return [] 188 | except Exception as e: 189 | print(f"加载隐藏端口配置失败: {e}") 190 | return [] 191 | 192 | def save_hidden_ports(hidden_ports): 193 | """保存隐藏端口配置""" 194 | try: 195 | with open(HIDDEN_PORTS_FILE, 'w', encoding='utf-8') as f: 196 | json.dump(hidden_ports, f, indent=2, ensure_ascii=False) 197 | return True 198 | except Exception as e: 199 | print(f"保存隐藏端口配置失败: {e}") 200 | return False 201 | 202 | # 初始化配置 203 | init_config() 204 | config = load_config() 205 | 206 | class PortMonitor: 207 | """端口监控类""" 208 | 209 | def __init__(self): 210 | """初始化Docker客户端""" 211 | try: 212 | self.docker_client = docker.from_env() 213 | logger.info("Docker客户端连接成功") 214 | except Exception as e: 215 | logger.error(f"Docker客户端连接失败: {e}") 216 | self.docker_client = None 217 | 218 | # 缓存相关属性 219 | self.container_cache = {} # 容器信息缓存 220 | self.cache_timestamp = 0 # 缓存时间戳 221 | self.cache_ttl = 30 # 缓存生存时间(秒) 222 | 223 | # 默认端口服务映射 224 | self.default_ports = { 225 | 21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP", 53: "DNS", 67: "DHCP Server", 68: "DHCP Client", 226 | 69: "TFTP", 80: "HTTP", 110: "POP3", 123: "NTP", 135: "RPC", 137: "NetBIOS Name", 138: "NetBIOS Datagram", 227 | 139: "NetBIOS Session", 143: "IMAP", 161: "SNMP", 389: "LDAP", 443: "HTTPS", 445: "SMB", 465: "SMTPS", 228 | 514: "Syslog", 587: "SMTP", 631: "IPP", 636: "LDAPS", 993: "IMAPS", 995: "POP3S", 1433: "SQL Server", 229 | 1521: "Oracle", 3306: "MySQL", 3389: "RDP", 5432: "PostgreSQL", 5900: "VNC", 6379: "Redis", 230 | 8080: "HTTP Proxy", 8443: "HTTPS Alt", 9200: "Elasticsearch", 27017: "MongoDB" 231 | } 232 | 233 | def get_docker_ports(self): 234 | """获取Docker容器端口映射信息""" 235 | ports_info = [] 236 | 237 | if not self.docker_client: 238 | logger.warning("Docker客户端未连接") 239 | return ports_info 240 | 241 | try: 242 | containers = self.docker_client.containers.list() 243 | logger.info(f"发现 {len(containers)} 个运行中的容器") 244 | 245 | for container in containers: 246 | container_name = container.name 247 | ports = container.attrs.get('NetworkSettings', {}).get('Ports', {}) 248 | 249 | for container_port, host_bindings in ports.items(): 250 | if host_bindings: 251 | for binding in host_bindings: 252 | host_port = int(binding['HostPort']) 253 | ports_info.append({ 254 | 'port': host_port, 255 | 'container_name': container_name, 256 | 'container_port': container_port, 257 | 'type': 'docker_mapped' 258 | }) 259 | logger.debug(f"发现映射端口: {host_port} -> {container_name}:{container_port}") 260 | 261 | # 检查host网络模式的容器 262 | network_mode = container.attrs.get('HostConfig', {}).get('NetworkMode', '') 263 | if network_mode == 'host': 264 | ports_info.append({ 265 | 'port': None, # host模式下无法直接获取端口 266 | 'container_name': container_name, 267 | 'container_port': 'host模式', 268 | 'type': 'docker_host' 269 | }) 270 | logger.debug(f"发现host模式容器: {container_name}") 271 | 272 | except Exception as e: 273 | logger.error(f"获取Docker端口信息失败: {e}") 274 | 275 | return ports_info 276 | 277 | def get_host_ports(self): 278 | """获取主机端口使用情况(简化版本,仅检测端口占用)""" 279 | port_info = {} 280 | port_protocols = {} # 用于跟踪每个端口的协议和IP版本 281 | 282 | # 获取host网络容器信息 283 | host_containers = self.get_host_network_containers_cached() 284 | 285 | try: 286 | # 使用netstat获取监听端口信息(不获取进程信息) 287 | result = subprocess.run( 288 | ['netstat', '-tuln'], 289 | capture_output=True, 290 | text=True, 291 | check=True 292 | ) 293 | 294 | logger.info("成功执行netstat命令") 295 | 296 | # 解析netstat输出 297 | for line in result.stdout.split('\n'): 298 | if not line.strip(): 299 | continue 300 | 301 | parts = line.split() 302 | if len(parts) < 4: 303 | continue 304 | 305 | # 匹配监听端口行 306 | if 'LISTEN' in line or 'udp' in line: 307 | protocol = parts[0].upper() # TCP/UDP 308 | local_address = parts[3] 309 | 310 | # 解析协议和端口号 311 | # 根据协议名称确定IP版本和协议类型 312 | if protocol.endswith('6'): 313 | # TCP6/UDP6 表示IPv6 314 | protocol_type = protocol[:-1] # 去掉末尾的6 315 | ip_version = 'IPv6' 316 | else: 317 | # TCP/UDP 表示IPv4 318 | protocol_type = protocol 319 | ip_version = 'IPv4' 320 | 321 | if ':' in local_address: 322 | # 解析端口号 323 | if local_address.count(':') > 1 or protocol.endswith('6'): 324 | # IPv6地址格式: [::]:port 或 [address]:port 325 | if local_address.startswith('['): 326 | port_part = local_address.split(']:')[-1] 327 | else: 328 | port_part = local_address.split(':')[-1] 329 | else: 330 | # IPv4格式: address:port 331 | port_part = local_address.split(':')[-1] 332 | 333 | try: 334 | port = int(port_part) 335 | except ValueError: 336 | continue 337 | else: 338 | continue 339 | 340 | # 检查是否为host网络容器的端口 341 | container_name = None 342 | for container_info in host_containers.values(): 343 | if port in container_info['exposed_ports']: 344 | container_name = container_info['name'] 345 | break 346 | 347 | # 跟踪端口的协议和IP版本 348 | if port not in port_protocols: 349 | port_protocols[port] = {'protocols': set(), 'ip_versions': set()} 350 | 351 | port_protocols[port]['protocols'].add(protocol_type) 352 | port_protocols[port]['ip_versions'].add(ip_version) 353 | 354 | # 如果端口已存在,更新信息 355 | if port not in port_info: 356 | port_info[port] = { 357 | 'port': port, 358 | 'protocol': protocol_type, 359 | 'ip_version': ip_version, 360 | 'address': local_address, 361 | 'service_name': self.get_service_name(port), 362 | 'container_name': container_name 363 | } 364 | 365 | logger.debug(f"发现主机使用端口: {port} ({protocol}/{ip_version})") 366 | 367 | # 合并协议信息 368 | for port, info in port_info.items(): 369 | protocols = port_protocols[port]['protocols'] 370 | ip_versions = port_protocols[port]['ip_versions'] 371 | 372 | # 合并协议,包含IP版本信息 373 | protocol_list = [] 374 | for protocol in sorted(protocols): 375 | if 'IPv4' in ip_versions and 'IPv6' in ip_versions: 376 | # 同时支持IPv4和IPv6,显示TCP/UDP和TCP6/UDP6 377 | protocol_list.extend([protocol, protocol + '6']) 378 | elif 'IPv6' in ip_versions: 379 | # 只支持IPv6 380 | protocol_list.append(protocol + '6') 381 | else: 382 | # 只支持IPv4 383 | protocol_list.append(protocol) 384 | 385 | # 去重并排序 386 | protocol_list = sorted(list(set(protocol_list))) 387 | info['protocol'] = '/'.join(protocol_list) 388 | 389 | # 移除单独的ip_version字段,信息已包含在protocol中 390 | del info['ip_version'] 391 | 392 | except subprocess.CalledProcessError as e: 393 | logger.error(f"执行netstat命令失败: {e}") 394 | except Exception as e: 395 | logger.error(f"获取主机端口信息失败: {e}") 396 | 397 | return port_info 398 | 399 | def get_service_name(self, port): 400 | """根据端口号获取服务名称(仅使用配置文件映射)""" 401 | # 从配置文件获取端口映射,适配新的数据结构 402 | config_ports = {} 403 | for k, v in config.items(): 404 | if isinstance(v, dict) and 'port' in v: 405 | config_ports[k] = v['port'] 406 | elif isinstance(v, int): 407 | config_ports[k] = v 408 | 409 | # 创建端口到服务名的映射(反向映射) 410 | port_to_service = {v: k for k, v in config_ports.items()} 411 | 412 | # 使用配置文件中的端口映射 413 | if port in port_to_service: 414 | return port_to_service[port] 415 | 416 | # 使用默认端口映射 417 | if port in self.default_ports: 418 | return self.default_ports[port] 419 | 420 | # 如果都没有,返回未知 421 | return '未知服务' 422 | 423 | def get_host_network_containers_cached(self): 424 | """获取host网络容器信息(带缓存,增强版本)""" 425 | import time 426 | import re 427 | 428 | current_time = time.time() 429 | 430 | # 检查缓存是否有效 431 | if (current_time - self.cache_timestamp) < self.cache_ttl and self.container_cache: 432 | logger.debug("使用缓存的容器信息") 433 | return self.container_cache 434 | 435 | logger.debug("刷新容器信息缓存") 436 | self.container_cache = {} 437 | 438 | if not self.docker_client: 439 | return self.container_cache 440 | 441 | try: 442 | containers = self.docker_client.containers.list() 443 | for container in containers: 444 | # 检查容器的网络模式 445 | network_mode = container.attrs.get('HostConfig', {}).get('NetworkMode', '') 446 | if network_mode == 'host': 447 | container_info = { 448 | 'name': container.name, 449 | 'id': container.id[:12], 450 | 'image': container.image.tags[0] if container.image.tags else 'unknown', 451 | 'exposed_ports': set(), 452 | 'potential_ports': set(), # 从其他配置推断的可能端口 453 | 'healthcheck_ports': set(), # 从健康检查推断的端口 454 | 'entrypoint_ports': set() # 从入口点推断的端口 455 | } 456 | 457 | # 1. 获取容器的ExposedPorts 458 | try: 459 | exposed_ports = container.attrs.get('Config', {}).get('ExposedPorts', {}) 460 | if exposed_ports: 461 | for port_spec in exposed_ports.keys(): 462 | # 解析端口格式,如 "80/tcp", "53/udp" 463 | if '/' in port_spec: 464 | port_num = int(port_spec.split('/')[0]) 465 | container_info['exposed_ports'].add(port_num) 466 | logger.debug(f"容器 {container.name} 暴露端口: {port_num}") 467 | except Exception as e: 468 | logger.debug(f"获取容器 {container.name} ExposedPorts失败: {e}") 469 | 470 | # 2. 检查Healthcheck配置中的端口 471 | try: 472 | healthcheck = container.attrs.get('Config', {}).get('Healthcheck', {}) 473 | if healthcheck and 'Test' in healthcheck: 474 | test_cmd = ' '.join(healthcheck['Test']) if isinstance(healthcheck['Test'], list) else str(healthcheck['Test']) 475 | # 使用正则表达式查找端口号 476 | port_matches = re.findall(r'(?:localhost|127\.0\.0\.1|0\.0\.0\.0):?(\d{1,5})', test_cmd) 477 | for port_str in port_matches: 478 | try: 479 | port_num = int(port_str) 480 | if 1 <= port_num <= 65535: 481 | container_info['healthcheck_ports'].add(port_num) 482 | container_info['potential_ports'].add(port_num) 483 | logger.debug(f"容器 {container.name} 健康检查端口: {port_num}") 484 | except ValueError: 485 | continue 486 | except Exception as e: 487 | logger.debug(f"获取容器 {container.name} Healthcheck失败: {e}") 488 | 489 | # 3. 检查Entrypoint和Cmd中的端口 490 | try: 491 | # 检查Entrypoint 492 | entrypoint = container.attrs.get('Config', {}).get('Entrypoint', []) 493 | cmd = container.attrs.get('Config', {}).get('Cmd', []) 494 | 495 | # 合并entrypoint和cmd 496 | full_command = [] 497 | if entrypoint: 498 | full_command.extend(entrypoint if isinstance(entrypoint, list) else [entrypoint]) 499 | if cmd: 500 | full_command.extend(cmd if isinstance(cmd, list) else [cmd]) 501 | 502 | command_str = ' '.join(str(arg) for arg in full_command) 503 | 504 | # 查找常见的端口参数模式 505 | port_patterns = [ 506 | r'--port[=\s]+(\d{1,5})', # --port=8080 或 --port 8080 507 | r'-p[=\s]+(\d{1,5})', # -p=8080 或 -p 8080 508 | r'--listen[=\s]+(\d{1,5})', # --listen=8080 509 | r'--bind[=\s]+[^:]*:(\d{1,5})', # --bind=0.0.0.0:8080 510 | r':(\d{1,5})\b', # 通用的 :端口 模式 511 | r'PORT[=\s]+(\d{1,5})', # PORT=8080 512 | r'HTTP_PORT[=\s]+(\d{1,5})', # HTTP_PORT=8080 513 | ] 514 | 515 | for pattern in port_patterns: 516 | matches = re.findall(pattern, command_str, re.IGNORECASE) 517 | for port_str in matches: 518 | try: 519 | port_num = int(port_str) 520 | if 1 <= port_num <= 65535: 521 | container_info['entrypoint_ports'].add(port_num) 522 | container_info['potential_ports'].add(port_num) 523 | logger.debug(f"容器 {container.name} 入口点端口: {port_num}") 524 | except ValueError: 525 | continue 526 | 527 | except Exception as e: 528 | logger.debug(f"获取容器 {container.name} Entrypoint/Cmd失败: {e}") 529 | 530 | # 4. 检查环境变量中的端口 531 | try: 532 | env_vars = container.attrs.get('Config', {}).get('Env', []) 533 | for env_var in env_vars: 534 | if '=' in env_var: 535 | key, value = env_var.split('=', 1) 536 | # 查找端口相关的环境变量 537 | if any(port_keyword in key.upper() for port_keyword in ['PORT', 'LISTEN', 'BIND']): 538 | try: 539 | # 尝试从环境变量值中提取端口号 540 | port_matches = re.findall(r'\b(\d{1,5})\b', value) 541 | for port_str in port_matches: 542 | port_num = int(port_str) 543 | if 1 <= port_num <= 65535: 544 | container_info['potential_ports'].add(port_num) 545 | logger.debug(f"容器 {container.name} 环境变量端口: {port_num} (来自 {key})") 546 | except (ValueError, AttributeError): 547 | continue 548 | except Exception as e: 549 | logger.debug(f"获取容器 {container.name} 环境变量失败: {e}") 550 | 551 | # 合并所有端口到exposed_ports中 552 | container_info['exposed_ports'].update(container_info['potential_ports']) 553 | 554 | self.container_cache[container.name] = container_info 555 | 556 | except Exception as e: 557 | logger.error(f"获取Docker容器信息失败: {e}") 558 | 559 | self.cache_timestamp = current_time 560 | return self.container_cache 561 | 562 | def get_port_analysis(self, start_port=1, end_port=65535, protocol_filter=None): 563 | """分析端口使用情况并生成可视化数据""" 564 | docker_ports = self.get_docker_ports() 565 | host_ports_info = self.get_host_ports() 566 | 567 | # 初始化端口卡片列表 568 | port_cards = [] 569 | 570 | # 分别处理TCP和UDP端口 571 | tcp_ports = set() 572 | udp_ports = set() 573 | port_protocol_map = {} # 端口到协议的映射 574 | 575 | # 处理主机端口信息,区分TCP和UDP,并应用端口范围过滤 576 | for port, info in host_ports_info.items(): 577 | # 应用端口范围过滤 578 | if port < start_port or port > end_port: 579 | continue 580 | 581 | protocol = info.get('protocol', 'TCP') 582 | port_protocol_map[port] = protocol 583 | 584 | # 根据协议分类端口 585 | if 'TCP' in protocol.upper(): 586 | tcp_ports.add(port) 587 | if 'UDP' in protocol.upper(): 588 | udp_ports.add(port) 589 | 590 | # 处理Docker端口(通常是TCP),并应用端口范围过滤 591 | docker_port_map = {} 592 | for port_info in docker_ports: 593 | if port_info['port']: 594 | port = port_info['port'] 595 | # 应用端口范围过滤 596 | if port < start_port or port > end_port: 597 | continue 598 | 599 | tcp_ports.add(port) # Docker端口映射通常是TCP 600 | docker_port_map[port] = port_info 601 | if port not in port_protocol_map: 602 | port_protocol_map[port] = 'TCP' 603 | 604 | # 根据协议过滤器选择端口 605 | if protocol_filter == 'TCP': 606 | filtered_ports = tcp_ports 607 | logger.info(f"TCP协议过滤: 发现 {len(tcp_ports)} 个TCP端口") 608 | elif protocol_filter == 'UDP': 609 | filtered_ports = udp_ports 610 | logger.info(f"UDP协议过滤: 发现 {len(udp_ports)} 个UDP端口") 611 | else: 612 | # 显示所有端口 613 | filtered_ports = tcp_ports.union(udp_ports) 614 | logger.info(f"总共发现 {len(filtered_ports)} 个已使用端口 (TCP: {len(tcp_ports)}, UDP: {len(udp_ports)})") 615 | 616 | sorted_ports = sorted(filtered_ports) 617 | 618 | # 预处理:收集所有端口信息 619 | port_data_list = [] 620 | for port in sorted_ports: 621 | protocol = port_protocol_map.get(port, 'TCP') 622 | 623 | # 如果有协议过滤器,跳过不匹配的端口 624 | if protocol_filter and protocol_filter.upper() not in protocol.upper(): 625 | continue 626 | 627 | # 检查配置文件中是否有该端口的service_type信息 628 | config_service_type = None 629 | config_service_name = None 630 | for service_name, service_config in config.items(): 631 | if isinstance(service_config, dict) and service_config.get('port') == port: 632 | config_service_type = service_config.get('service_type') 633 | config_service_name = service_name 634 | break 635 | 636 | if port in docker_port_map: 637 | # Docker容器端口 638 | docker_info = docker_port_map[port] 639 | # 如果配置文件中指定了service_type,使用配置文件的;否则默认为docker 640 | source = config_service_type if config_service_type in ['docker', 'host'] else 'docker' 641 | card_data = { 642 | 'port': port, 643 | 'type': 'used', 644 | 'source': source, 645 | 'protocol': protocol, 646 | 'container': docker_info['container_name'], 647 | 'process': f"Docker: {docker_info['container_name']}", 648 | 'image': docker_info.get('image', ''), 649 | 'container_port': docker_info['container_port'], 650 | 'service_name': config_service_name or docker_info['container_name'] 651 | } 652 | else: 653 | # 系统服务端口 654 | host_info = host_ports_info.get(port, {}) 655 | 656 | # 检查是否为host网络容器 657 | is_host_container = bool(host_info.get('container_name')) 658 | 659 | # 确定source:优先使用配置文件中的service_type 660 | if config_service_type in ['docker', 'host']: 661 | source = config_service_type 662 | elif is_host_container: 663 | source = 'docker' 664 | else: 665 | source = 'system' 666 | 667 | card_data = { 668 | 'port': port, 669 | 'type': 'used', 670 | 'source': source, 671 | 'protocol': protocol, 672 | 'service_name': config_service_name or host_info.get('service_name', '未知服务'), 673 | 'container': host_info.get('container_name'), 674 | 'is_host_network': is_host_container 675 | } 676 | port_data_list.append(card_data) 677 | 678 | # 处理连续的未知端口合并 679 | i = 0 680 | while i < len(port_data_list): 681 | current_port_data = port_data_list[i] 682 | 683 | # 检查是否为未知服务且可以开始合并 684 | if current_port_data['service_name'] == '未知服务': 685 | # 查找连续的未知端口 686 | consecutive_unknown = [current_port_data] 687 | j = i + 1 688 | 689 | while (j < len(port_data_list) and 690 | port_data_list[j]['service_name'] == '未知服务' and 691 | port_data_list[j]['port'] == port_data_list[j-1]['port'] + 1): 692 | consecutive_unknown.append(port_data_list[j]) 693 | j += 1 694 | 695 | # 如果有连续的未知端口(2个或以上),则合并 696 | if len(consecutive_unknown) >= 2: 697 | range_start_port = consecutive_unknown[0]['port'] 698 | range_end_port = consecutive_unknown[-1]['port'] 699 | 700 | # 创建合并的端口卡片 701 | merged_card = { 702 | 'type': 'unknown_range', 703 | 'start_port': range_start_port, 704 | 'end_port': range_end_port, 705 | 'port_count': len(consecutive_unknown), 706 | 'source': consecutive_unknown[0]['source'], 707 | 'protocol': consecutive_unknown[0]['protocol'], 708 | 'service_name': '未知服务', 709 | 'container': consecutive_unknown[0].get('container'), 710 | 'is_host_network': consecutive_unknown[0].get('is_host_network', False) 711 | } 712 | port_cards.append(merged_card) 713 | 714 | # 跳过已处理的端口 715 | i = j 716 | else: 717 | # 单个未知端口,正常添加 718 | port_cards.append(current_port_data) 719 | i += 1 720 | else: 721 | # 非未知端口,正常添加 722 | port_cards.append(current_port_data) 723 | i += 1 724 | 725 | # 检查是否需要添加间隔卡片 726 | if i < len(port_data_list): 727 | # 获取当前卡片的最后一个端口 728 | last_card = port_cards[-1] 729 | if last_card['type'] == 'unknown_range': 730 | current_last_port = last_card['end_port'] 731 | else: 732 | current_last_port = last_card.get('port') 733 | 734 | next_port = port_data_list[i]['port'] 735 | gap = next_port - current_last_port - 1 736 | 737 | if gap > 0: 738 | gap_card = { 739 | 'type': 'gap', 740 | 'start_port': current_last_port + 1, 741 | 'end_port': next_port - 1, 742 | 'available_count': gap 743 | } 744 | port_cards.append(gap_card) 745 | 746 | # 添加最后一个端口到65535的间隙 747 | if port_cards: 748 | # 获取最后一个卡片的最后端口 749 | last_card = port_cards[-1] 750 | 751 | if last_card['type'] == 'gap': 752 | # 如果最后一个是gap卡片,检查是否到达65535 753 | if last_card['end_port'] < end_port: 754 | # 更新最后一个gap卡片到65535 755 | last_card['end_port'] = end_port 756 | last_card['available_count'] = last_card['end_port'] - last_card['start_port'] + 1 757 | else: 758 | if last_card['type'] == 'unknown_range': 759 | last_port = last_card['end_port'] 760 | else: 761 | last_port = last_card.get('port', 0) 762 | 763 | if last_port < end_port: 764 | final_gap = end_port - last_port 765 | if final_gap > 0: 766 | gap_card = { 767 | 'type': 'gap', 768 | 'start_port': last_port + 1, 769 | 'end_port': end_port, 770 | 'available_count': final_gap 771 | } 772 | port_cards.append(gap_card) 773 | else: 774 | # 如果没有任何端口卡片,创建一个从1到65535的完整gap 775 | gap_card = { 776 | 'type': 'gap', 777 | 'start_port': start_port, 778 | 'end_port': end_port, 779 | 'available_count': end_port - start_port + 1 780 | } 781 | port_cards.append(gap_card) 782 | 783 | # 统计Docker容器数量 784 | docker_container_count = len(set( 785 | p.get('container', p.get('container_name', '')) 786 | for p in port_cards 787 | if p.get('source') == 'docker' and p.get('container') 788 | )) 789 | 790 | # 计算可用端口数量(基于指定的端口范围) 791 | total_ports_in_range = end_port - start_port + 1 792 | if protocol_filter: 793 | # 如果有协议过滤器,可用端口数量是范围内总端口数减去该协议的已使用端口数 794 | available_ports = total_ports_in_range - len(filtered_ports) 795 | else: 796 | # 显示所有协议时,可用端口数量是范围内总端口数减去所有已使用端口数 797 | all_used_ports = tcp_ports.union(udp_ports) 798 | available_ports = total_ports_in_range - len(all_used_ports) 799 | 800 | # 过滤隐藏的端口 801 | hidden_ports = load_hidden_ports() 802 | if hidden_ports: 803 | filtered_port_cards = [] 804 | for card in port_cards: 805 | should_hide = False 806 | 807 | if card['type'] == 'used': 808 | # 检查单个端口是否被隐藏 809 | if card['port'] in hidden_ports: 810 | should_hide = True 811 | elif card['type'] == 'unknown_range': 812 | # 检查端口范围是否有任何端口被隐藏 813 | for port in range(card['start_port'], card['end_port'] + 1): 814 | if port in hidden_ports: 815 | should_hide = True 816 | break 817 | 818 | if not should_hide: 819 | filtered_port_cards.append(card) 820 | 821 | port_cards = filtered_port_cards 822 | 823 | return { 824 | 'port_cards': port_cards, 825 | 'total_used': len(filtered_ports), 826 | 'total_available': available_ports, 827 | 'tcp_used': len(tcp_ports), 828 | 'udp_used': len(udp_ports), 829 | 'docker_containers': docker_container_count, 830 | 'hidden_ports': hidden_ports, 831 | 'protocol_filter': protocol_filter 832 | } 833 | 834 | # 创建端口监控实例 835 | port_monitor = PortMonitor() 836 | 837 | @app.route('/') 838 | def index(): 839 | """主页面""" 840 | return render_template('index.html') 841 | 842 | @app.route('/api/ports') 843 | def api_ports(): 844 | """获取端口信息API""" 845 | try: 846 | # 获取协议过滤器参数 847 | protocol_filter = request.args.get('protocol', '').strip().upper() 848 | if protocol_filter not in ['TCP', 'UDP', '']: 849 | protocol_filter = None 850 | 851 | # 获取端口范围参数 852 | start_port = request.args.get('start_port', '1') 853 | end_port = request.args.get('end_port', '65535') 854 | 855 | # 验证端口范围参数 856 | try: 857 | start_port = int(start_port) 858 | end_port = int(end_port) 859 | 860 | # 确保端口范围有效 861 | if start_port < 1: 862 | start_port = 1 863 | if end_port > 65535: 864 | end_port = 65535 865 | if start_port > end_port: 866 | start_port, end_port = end_port, start_port 867 | 868 | except ValueError: 869 | # 如果参数无效,使用默认范围 870 | start_port = 1 871 | end_port = 65535 872 | 873 | port_data = port_monitor.get_port_analysis(start_port=start_port, end_port=end_port, protocol_filter=protocol_filter) 874 | 875 | # 处理搜索参数 876 | search = request.args.get('search', '').strip().lower() 877 | if search: 878 | # 保存原始的总已使用端口数 879 | original_total_used = port_data['total_used'] 880 | 881 | filtered_cards = [] 882 | for card in port_data['port_cards']: 883 | if card['type'] == 'used': 884 | # 搜索端口号、进程名、服务名、容器名、协议 885 | searchable_text = ' '.join([ 886 | str(card.get('port', '')), 887 | card.get('process', '') or '', 888 | card.get('service_name', '') or '', 889 | card.get('container', '') or '', 890 | card.get('protocol', '') or '' 891 | ]).lower() 892 | 893 | if search in searchable_text: 894 | filtered_cards.append(card) 895 | elif card['type'] == 'unknown_range': 896 | # 搜索端口范围、服务名、容器名、协议 897 | searchable_text = ' '.join([ 898 | f"{card.get('start_port', '')}-{card.get('end_port', '')}", 899 | str(card.get('start_port', '')), 900 | str(card.get('end_port', '')), 901 | card.get('service_name', '') or '', 902 | card.get('container', '') or '', 903 | card.get('protocol', '') or '' 904 | ]).lower() 905 | 906 | # 检查是否搜索范围内的单个端口号 907 | is_match = search in searchable_text 908 | if not is_match and search.isdigit(): 909 | search_port = int(search) 910 | card_start_port = card.get('start_port', 0) 911 | card_end_port = card.get('end_port', 0) 912 | if card_start_port <= search_port <= card_end_port: 913 | is_match = True 914 | 915 | if is_match: 916 | filtered_cards.append(card) 917 | elif card['type'] == 'gap': 918 | # 搜索可用端口范围 919 | searchable_text = ' '.join([ 920 | f"{card.get('start_port', '')}-{card.get('end_port', '')}", 921 | str(card.get('start_port', '')), 922 | str(card.get('end_port', '')), 923 | '可用', 'available', 'unused' 924 | ]).lower() 925 | 926 | # 检查是否搜索范围内的单个端口号 927 | is_match = search in searchable_text 928 | if not is_match and search.isdigit(): 929 | search_port = int(search) 930 | gap_start_port = card.get('start_port', 0) 931 | gap_end_port = card.get('end_port', 0) 932 | if gap_start_port <= search_port <= gap_end_port: 933 | is_match = True 934 | 935 | if is_match: 936 | filtered_cards.append(card) 937 | 938 | # 按端口排序 939 | filtered_cards = sorted(filtered_cards, key=lambda x: x.get('port', x.get('start_port', 0))) 940 | 941 | # 计算搜索结果中的已使用端口数 942 | filtered_used_count = len([card for card in filtered_cards if card['type'] in ['used', 'unknown_range']]) 943 | 944 | # 更新统计信息 945 | port_data['port_cards'] = filtered_cards 946 | port_data['total_used'] = filtered_used_count 947 | # 搜索时,可用端口数量应该是总端口数减去所有已使用的端口数,而不是搜索结果数 948 | port_data['total_available'] = max(0, 65535 - original_total_used) 949 | 950 | return jsonify({ 951 | 'success': True, 952 | 'data': port_data 953 | }) 954 | except Exception as e: 955 | logger.error(f"API调用失败: {e}") 956 | return jsonify({ 957 | 'success': False, 958 | 'error': str(e) 959 | }), 500 960 | 961 | @app.route('/api/config') 962 | def api_get_config(): 963 | """API接口:获取配置信息""" 964 | try: 965 | return jsonify(config) 966 | except Exception as e: 967 | return jsonify({'error': str(e)}), 500 968 | 969 | @app.route('/api/config/raw') 970 | def api_get_raw_config(): 971 | """API接口:获取原始配置文件内容(用于设置界面编辑)""" 972 | try: 973 | with open(CONFIG_FILE, 'r', encoding='utf-8') as f: 974 | raw_config = json.load(f) 975 | return jsonify(raw_config) 976 | except Exception as e: 977 | logger.error(f"获取原始配置失败: {e}") 978 | return jsonify({'error': str(e)}), 500 979 | 980 | @app.route('/api/config', methods=['POST']) 981 | def api_save_config(): 982 | """API接口:保存配置信息""" 983 | # 重新加载配置 984 | global config 985 | 986 | try: 987 | data = request.get_json() 988 | if not data: 989 | return jsonify({'error': '无效的配置数据'}), 400 990 | 991 | # 检查是否是添加单个端口的请求 992 | if 'port' in data and 'service_name' in data: 993 | port = data['port'] 994 | service_name = data['service_name'].strip() 995 | service_type = data.get('service_type', 'host') # 默认为host 996 | 997 | if not service_name: 998 | return jsonify({'error': '服务名称不能为空'}), 400 999 | 1000 | # 验证端口号 1001 | if not isinstance(port, int) or port < 1 or port > 65535: 1002 | return jsonify({'error': '端口号必须在1-65535之间'}), 400 1003 | 1004 | # 验证服务类型 1005 | if service_type not in ['docker', 'host']: 1006 | return jsonify({'error': '服务类型必须是docker或host'}), 400 1007 | 1008 | # 加载当前配置 1009 | current_config = load_config() 1010 | 1011 | # 检查端口是否已存在,适配新的数据结构 1012 | existing_service = None 1013 | for service, config_value in current_config.items(): 1014 | existing_port = None 1015 | if isinstance(config_value, dict) and 'port' in config_value: 1016 | existing_port = config_value['port'] 1017 | elif isinstance(config_value, int): 1018 | existing_port = config_value 1019 | 1020 | if existing_port == port: 1021 | existing_service = service 1022 | break 1023 | 1024 | if existing_service: 1025 | # 更新现有端口的服务名称 1026 | del current_config[existing_service] 1027 | current_config[service_name] = { 1028 | 'port': port, 1029 | 'protocol': 'TCP', 1030 | 'service_type': service_type 1031 | } 1032 | else: 1033 | # 添加新的端口配置 1034 | current_config[service_name] = { 1035 | 'port': port, 1036 | 'protocol': 'TCP', 1037 | 'service_type': service_type 1038 | } 1039 | 1040 | # 保存配置 1041 | if save_config(current_config): 1042 | config = load_config() 1043 | return jsonify({ 1044 | 'success': True, 1045 | 'message': f'端口 {port} 的服务名称已设置为 "{service_name}"({service_type})' 1046 | }) 1047 | else: 1048 | return jsonify({'error': '配置保存失败'}), 500 1049 | else: 1050 | # 保存整个配置(原有功能)- 支持混合格式 1051 | # 验证配置格式(支持混合格式) 1052 | for name, value in data.items(): 1053 | if name == 'app_settings': 1054 | continue 1055 | 1056 | port = None 1057 | 1058 | if isinstance(value, int): 1059 | # 纯数字格式 1060 | port = value 1061 | elif isinstance(value, str): 1062 | # 字符串格式:"端口号:协议" 或 "端口号" 1063 | parts = value.split(':') 1064 | if len(parts) >= 1: 1065 | try: 1066 | port = int(parts[0]) 1067 | except ValueError: 1068 | return jsonify({'error': f'配置项 "{name}" 的端口号 "{parts[0]}" 无效'}), 400 1069 | 1070 | # 验证协议(如果存在) 1071 | if len(parts) > 1: 1072 | protocol = parts[1].lower() 1073 | if protocol not in ['tcp', 'udp']: 1074 | return jsonify({'error': f'配置项 "{name}" 的协议 "{parts[1]}" 无效,只支持TCP或UDP'}), 400 1075 | else: 1076 | return jsonify({'error': f'配置项 "{name}" 格式无效'}), 400 1077 | elif isinstance(value, dict): 1078 | # 对象格式:{port: 端口号, protocol: 协议} 1079 | if 'port' not in value: 1080 | return jsonify({'error': f'配置项 "{name}" 缺少端口号'}), 400 1081 | port = value['port'] 1082 | 1083 | # 验证协议(如果存在) 1084 | if 'protocol' in value: 1085 | protocol = str(value['protocol']).lower() 1086 | if protocol not in ['tcp', 'udp']: 1087 | return jsonify({'error': f'配置项 "{name}" 的协议 "{value["protocol"]}" 无效,只支持TCP或UDP'}), 400 1088 | else: 1089 | return jsonify({'error': f'配置项 "{name}" 格式无效,支持格式:端口号、"端口号:协议" 或 {{port: 端口号, protocol: 协议}}'}), 400 1090 | 1091 | if not isinstance(port, int) or port < 1 or port > 65535: 1092 | return jsonify({'error': f'端口号 "{port}" 无效,必须是1-65535之间的整数'}), 400 1093 | 1094 | # 直接保存原始格式的配置到文件 1095 | try: 1096 | with open(CONFIG_FILE, 'w', encoding='utf-8') as f: 1097 | json.dump(data, f, ensure_ascii=False, indent=2) 1098 | 1099 | # 更新全局配置(重新加载以确保一致性) 1100 | config = load_config() 1101 | 1102 | logger.info("配置已更新") 1103 | return jsonify({'success': True, 'message': '配置保存成功'}) 1104 | except Exception as e: 1105 | logger.error(f"写入配置文件失败: {e}") 1106 | return jsonify({'error': '配置保存失败'}), 500 1107 | 1108 | except Exception as e: 1109 | logger.error(f"保存配置时出错: {e}") 1110 | return jsonify({'error': str(e)}), 500 1111 | 1112 | @app.route('/api/refresh') 1113 | def api_refresh(): 1114 | """刷新端口信息API""" 1115 | try: 1116 | # 重新初始化Docker客户端 1117 | port_monitor.__init__() 1118 | port_data = port_monitor.get_port_analysis() 1119 | return jsonify({ 1120 | 'success': True, 1121 | 'data': port_data, 1122 | 'message': '端口信息已刷新' 1123 | }) 1124 | except Exception as e: 1125 | logger.error(f"刷新失败: {e}") 1126 | return jsonify({ 1127 | 'success': False, 1128 | 'error': str(e) 1129 | }), 500 1130 | 1131 | @app.route('/api/hidden-ports') 1132 | def api_get_hidden_ports(): 1133 | """获取隐藏端口列表API""" 1134 | try: 1135 | hidden_ports = load_hidden_ports() 1136 | return jsonify({ 1137 | 'success': True, 1138 | 'data': hidden_ports 1139 | }) 1140 | except Exception as e: 1141 | logger.error(f"获取隐藏端口失败: {e}") 1142 | return jsonify({ 1143 | 'success': False, 1144 | 'error': str(e) 1145 | }), 500 1146 | 1147 | @app.route('/api/hidden-ports', methods=['POST']) 1148 | def api_hide_port(): 1149 | """隐藏端口API""" 1150 | try: 1151 | data = request.get_json() 1152 | if not data or 'port' not in data: 1153 | return jsonify({'error': '缺少端口参数'}), 400 1154 | 1155 | port = data['port'] 1156 | if not isinstance(port, int) or port < 1 or port > 65535: 1157 | return jsonify({'error': '端口号必须在1-65535之间'}), 400 1158 | 1159 | hidden_ports = load_hidden_ports() 1160 | if port not in hidden_ports: 1161 | hidden_ports.append(port) 1162 | hidden_ports.sort() 1163 | 1164 | if save_hidden_ports(hidden_ports): 1165 | return jsonify({ 1166 | 'success': True, 1167 | 'message': f'端口 {port} 已隐藏' 1168 | }) 1169 | else: 1170 | return jsonify({'error': '保存隐藏端口配置失败'}), 500 1171 | else: 1172 | return jsonify({ 1173 | 'success': True, 1174 | 'message': f'端口 {port} 已经被隐藏' 1175 | }) 1176 | 1177 | except Exception as e: 1178 | logger.error(f"隐藏端口失败: {e}") 1179 | return jsonify({ 1180 | 'success': False, 1181 | 'error': str(e) 1182 | }), 500 1183 | 1184 | @app.route('/api/hidden-ports', methods=['DELETE']) 1185 | def api_unhide_port(): 1186 | """取消隐藏端口API""" 1187 | try: 1188 | data = request.get_json() 1189 | if not data or 'port' not in data: 1190 | return jsonify({'error': '缺少端口参数'}), 400 1191 | 1192 | port = data['port'] 1193 | if not isinstance(port, int) or port < 1 or port > 65535: 1194 | return jsonify({'error': '端口号必须在1-65535之间'}), 400 1195 | 1196 | hidden_ports = load_hidden_ports() 1197 | if port in hidden_ports: 1198 | hidden_ports.remove(port) 1199 | 1200 | if save_hidden_ports(hidden_ports): 1201 | return jsonify({ 1202 | 'success': True, 1203 | 'message': f'端口 {port} 已取消隐藏' 1204 | }) 1205 | else: 1206 | return jsonify({'error': '保存隐藏端口配置失败'}), 500 1207 | else: 1208 | return jsonify({ 1209 | 'success': True, 1210 | 'message': f'端口 {port} 未被隐藏' 1211 | }) 1212 | 1213 | except Exception as e: 1214 | logger.error(f"取消隐藏端口失败: {e}") 1215 | return jsonify({ 1216 | 'success': False, 1217 | 'error': str(e) 1218 | }), 500 1219 | 1220 | @app.route('/api/hidden-ports/batch', methods=['POST']) 1221 | def api_hide_ports_batch(): 1222 | """批量隐藏端口API""" 1223 | try: 1224 | data = request.get_json() 1225 | if not data or 'ports' not in data: 1226 | return jsonify({'error': '缺少端口列表参数'}), 400 1227 | 1228 | ports = data['ports'] 1229 | if not isinstance(ports, list): 1230 | return jsonify({'error': '端口列表必须是数组'}), 400 1231 | 1232 | # 验证所有端口号 1233 | for port in ports: 1234 | if not isinstance(port, int) or port < 1 or port > 65535: 1235 | return jsonify({'error': f'端口号 {port} 无效,必须在1-65535之间'}), 400 1236 | 1237 | hidden_ports = load_hidden_ports() 1238 | new_hidden_count = 0 1239 | 1240 | for port in ports: 1241 | if port not in hidden_ports: 1242 | hidden_ports.append(port) 1243 | new_hidden_count += 1 1244 | 1245 | hidden_ports.sort() 1246 | 1247 | if save_hidden_ports(hidden_ports): 1248 | return jsonify({ 1249 | 'success': True, 1250 | 'message': f'成功隐藏 {new_hidden_count} 个端口' 1251 | }) 1252 | else: 1253 | return jsonify({'error': '保存隐藏端口配置失败'}), 500 1254 | 1255 | except Exception as e: 1256 | logger.error(f"批量隐藏端口失败: {e}") 1257 | return jsonify({ 1258 | 'success': False, 1259 | 'error': str(e) 1260 | }), 500 1261 | 1262 | @app.route('/api/hidden-ports/batch', methods=['DELETE']) 1263 | def api_unhide_ports_batch(): 1264 | """批量取消隐藏端口API""" 1265 | try: 1266 | data = request.get_json() 1267 | if not data or 'ports' not in data: 1268 | return jsonify({'error': '缺少端口列表参数'}), 400 1269 | 1270 | ports = data['ports'] 1271 | if not isinstance(ports, list): 1272 | return jsonify({'error': '端口列表必须是数组'}), 400 1273 | 1274 | # 验证所有端口号 1275 | for port in ports: 1276 | if not isinstance(port, int) or port < 1 or port > 65535: 1277 | return jsonify({'error': f'端口号 {port} 无效,必须在1-65535之间'}), 400 1278 | 1279 | hidden_ports = load_hidden_ports() 1280 | removed_count = 0 1281 | 1282 | for port in ports: 1283 | if port in hidden_ports: 1284 | hidden_ports.remove(port) 1285 | removed_count += 1 1286 | 1287 | if save_hidden_ports(hidden_ports): 1288 | return jsonify({ 1289 | 'success': True, 1290 | 'message': f'成功取消隐藏 {removed_count} 个端口' 1291 | }) 1292 | else: 1293 | return jsonify({'error': '保存隐藏端口配置失败'}), 500 1294 | 1295 | except Exception as e: 1296 | logger.error(f"批量取消隐藏端口失败: {e}") 1297 | return jsonify({ 1298 | 'success': False, 1299 | 'error': str(e) 1300 | }), 500 1301 | 1302 | def parse_args(): 1303 | """解析命令行参数和环境变量""" 1304 | # 从环境变量获取默认值 1305 | default_port = int(os.environ.get('DOCKPORTS_PORT', 7577)) 1306 | default_host = os.environ.get('DOCKPORTS_HOST', '0.0.0.0') 1307 | default_debug = os.environ.get('DOCKPORTS_DEBUG', '').lower() in ('true', '1', 'yes') 1308 | 1309 | parser = argparse.ArgumentParser(description='DockPorts - 容器端口监控工具') 1310 | parser.add_argument('--port', '-p', type=int, default=default_port, 1311 | help=f'Web服务端口 (默认: {default_port}, 可通过环境变量DOCKPORTS_PORT设置)') 1312 | parser.add_argument('--host', type=str, default=default_host, 1313 | help=f'Web服务监听地址 (默认: {default_host}, 可通过环境变量DOCKPORTS_HOST设置)') 1314 | parser.add_argument('--debug', action='store_true', default=default_debug, 1315 | help='启用调试模式 (可通过环境变量DOCKPORTS_DEBUG=true设置)') 1316 | return parser.parse_args() 1317 | 1318 | if __name__ == '__main__': 1319 | # 解析命令行参数 1320 | args = parse_args() 1321 | 1322 | # 显示配置信息 1323 | logger.info("=== DockPorts 启动配置 ===") 1324 | logger.info(f"监听地址: {args.host}") 1325 | logger.info(f"监听端口: {args.port}") 1326 | logger.info(f"调试模式: {args.debug}") 1327 | 1328 | # 显示环境变量信息(用于调试) 1329 | env_port = os.environ.get('DOCKPORTS_PORT') 1330 | env_host = os.environ.get('DOCKPORTS_HOST') 1331 | env_debug = os.environ.get('DOCKPORTS_DEBUG') 1332 | 1333 | if env_port or env_host or env_debug: 1334 | logger.info("=== 环境变量配置 ===") 1335 | if env_port: 1336 | logger.info(f"DOCKPORTS_PORT: {env_port}") 1337 | if env_host: 1338 | logger.info(f"DOCKPORTS_HOST: {env_host}") 1339 | if env_debug: 1340 | logger.info(f"DOCKPORTS_DEBUG: {env_debug}") 1341 | 1342 | logger.info("=========================") 1343 | 1344 | # 验证端口范围 1345 | if not (1 <= args.port <= 65535): 1346 | logger.error(f"端口号 {args.port} 无效,必须在1-65535之间") 1347 | exit(1) 1348 | 1349 | try: 1350 | app.run(host=args.host, port=args.port, debug=args.debug) 1351 | except OSError as e: 1352 | if "Address already in use" in str(e): 1353 | logger.error(f"端口 {args.port} 已被占用,请使用 --port 参数指定其他端口") 1354 | logger.info("例如: python app.py --port 8080") 1355 | else: 1356 | logger.error(f"启动失败: {e}") 1357 | exit(1) 1358 | except KeyboardInterrupt: 1359 | logger.info("应用已停止") 1360 | except Exception as e: 1361 | logger.error(f"应用运行时出错: {e}") 1362 | exit(1) -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |容器端口监控与可视化工具
1190 |正在加载端口信息...
1310 |