Browse Source

add ipset service

xuqiang 4 tháng trước cách đây
mục cha
commit
facad492e6

+ 24 - 0
services/ipset/Makefile

@@ -0,0 +1,24 @@
+build:
+	pyinstaller ipset.spec
+
+release:
+	mkdir -p ./release/ipset
+	rm -rf ./release/ipset/*
+	pyinstaller ipset.spec
+	cp -f ./run.sh ./release/ipset
+	cp -f ./install.sh ./release/ipset
+	cp -r ./dist ./release/ipset/bin
+	cp -f ./ipset.service ./release/ipset
+
+install:
+	mkdir -p /usr/local/ipset
+	cp -r ./dist /usr/local/ipset/bin
+	cp -f ./run.sh /usr/local/ipset
+	cp ipset.service  /usr/lib/systemd/system
+	systemctl enable ipset
+	systemctl start ipset
+
+.PHONY:clean
+clean:
+	rm -rf build
+	rm -rf dist

+ 351 - 0
services/ipset/app.py

@@ -0,0 +1,351 @@
+#!/usr/bin/env python3
+from gevent import pywsgi
+from flask import Flask, request, jsonify, render_template_string
+import yaml
+import subprocess
+import threading
+import os
+import re
+import socket
+
+NETPLAN_FILE = "/etc/netplan/01-netcfg.yaml"
+PORT = 6000  # 定义端口常量
+
+app = Flask(__name__)
+
+# 前端 HTML 模板
+HTML_TEMPLATE = """
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>网络配置</title>
+<style>
+    body { font-family: Arial, sans-serif; margin: 20px; }
+    h2 { color: #333; }
+    input { 
+        margin: 5px 0 15px 0; 
+        padding: 8px; 
+        width: 300px; 
+        border: 1px solid #ccc; 
+        border-radius: 4px; 
+    }
+    button { 
+        padding: 10px 20px; 
+        background: #007bff; 
+        color: white; 
+        border: none; 
+        border-radius: 4px; 
+        cursor: pointer; 
+        font-size: 16px;
+    }
+    button:hover { background: #0056b3; }
+    .info { color: #666; margin-top: 10px; font-size: 14px; }
+    .countdown { color: #dc3545; font-weight: bold; }
+</style>
+</head>
+<body>
+<h2>网络配置</h2>
+<form id="netForm">
+    IP地址: <br><input id="ip" value="{{ ip }}"><br>
+    子网掩码位数: <br><input id="mask" value="{{ mask }}"><br>
+    网关: <br><input id="gateway" value="{{ gateway }}"><br>
+    DNS服务器: <br><input id="dns" value="{{ dns }}"><br>
+    <button type="button" onclick="save()">保存配置</button>
+    <div class="info">注意:保存后会有20秒时间确认,超时自动回滚</div>
+</form>
+
+<script>
+function save() {
+    const saveBtn = event.target;
+    const originalText = saveBtn.textContent;
+    const newIp = document.getElementById('ip').value;
+    
+    // 禁用按钮防止重复点击
+    saveBtn.disabled = true;
+    saveBtn.textContent = '保存中...';
+    saveBtn.style.background = '#6c757d';
+    
+    fetch('/api/network', {
+        method: 'POST',
+        headers: {'Content-Type': 'application/json'},
+        body: JSON.stringify({
+            ip: newIp,
+            mask: document.getElementById('mask').value,
+            gateway: document.getElementById('gateway').value,
+            dns: document.getElementById('dns').value
+        })
+    })
+    .then(res => res.json())
+    .then(data => {
+        if (data.success) {
+            // 保存成功,显示倒计时并跳转
+            let countdown = 15;
+            const message = data.status + '\\n\\n将在 ' + countdown + ' 秒后跳转到新地址...';
+            alert(message);
+            
+            // 创建倒计时显示
+            const countdownDiv = document.createElement('div');
+            countdownDiv.className = 'countdown';
+            countdownDiv.innerHTML = '正在跳转到新IP地址,请稍候... <span id="countdown">' + countdown + '</span> 秒';
+            document.body.appendChild(countdownDiv);
+            
+            // 倒计时并跳转
+            const countdownInterval = setInterval(() => {
+                countdown--;
+                document.getElementById('countdown').textContent = countdown;
+                
+                if (countdown <= 0) {
+                    clearInterval(countdownInterval);
+                    // 跳转到新的IP地址,包含端口号
+                    window.location.href = 'http://' + newIp + ':' + {{ port }};
+                }
+            }, 1000);
+            
+            // 同时尝试自动跳转(如果网络已生效)
+            setTimeout(() => {
+                window.location.href = 'http://' + newIp + ':' + {{ port }};
+            }, 3000);
+            
+        } else {
+            alert('保存失败: ' + data.status);
+        }
+    })
+    .catch(error => {
+        alert('保存失败: ' + error);
+    })
+    .finally(() => {
+        // 恢复按钮状态
+        saveBtn.disabled = false;
+        saveBtn.textContent = originalText;
+        saveBtn.style.background = '#007bff';
+    });
+}
+</script>
+</body>
+</html>
+"""
+
+def is_valid_ip(ip):
+    """验证IP地址格式"""
+    if not ip:
+        return False
+    pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
+    if re.match(pattern, ip):
+        parts = ip.split('.')
+        return all(0 <= int(part) <= 255 for part in parts)
+    return False
+
+def get_current_ip():
+    """获取当前服务器的IP地址"""
+    try:
+        # 获取本机IP
+        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        s.connect(("8.8.8.8", 80))
+        current_ip = s.getsockname()[0]
+        s.close()
+        return current_ip
+    except:
+        return "127.0.0.1"
+
+def read_netplan():
+    """读取当前网络配置"""
+    try:
+        with open(NETPLAN_FILE) as f:
+            data = yaml.safe_load(f)
+        
+        eth = data["network"]["ethernets"]["eth0"]
+        addresses = eth.get("addresses", [""])
+        
+        if addresses and addresses[0]:
+            ip = addresses[0].split("/")[0]
+            mask = addresses[0].split("/")[1]
+        else:
+            ip = ""
+            mask = "24"
+            
+        gateway = eth.get("routes", [{}])[0].get("via", "")
+        dns = eth.get("nameservers", {}).get("addresses", [""])[0]
+        
+        return {"ip": ip, "mask": mask, "gateway": gateway, "dns": dns}
+    except Exception as e:
+        print(f"读取配置文件出错: {e}")
+        return {"ip": "", "mask": "24", "gateway": "", "dns": "8.8.8.8"}
+
+def apply_netplan_async(new_ip):
+    """异步应用网络配置"""
+    try:
+        print(f"开始应用网络配置,新IP: {new_ip}")
+        
+        # 使用 netplan try 应用配置(20秒超时)
+        process = subprocess.Popen(
+            ["netplan", "try", "--timeout", "20"],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            text=True
+        )
+        
+        # 自动确认(按回车键)
+        try:
+            stdout, stderr = process.communicate(input='\n', timeout=5)
+            print(f"网络配置已应用,新IP: {new_ip}")
+            print(f"输出: {stdout}")
+            if stderr:
+                print(f"错误: {stderr}")
+                
+            # 重启网络服务确保配置生效
+            subprocess.run(["systemctl", "restart", "systemd-networkd"], 
+                         capture_output=True, text=True)
+            print("网络服务已重启")
+            
+        except subprocess.TimeoutExpired:
+            # 如果用户没有手动确认,会自动回滚
+            print("等待用户确认中...配置将在20秒后自动回滚")
+            process.kill()
+        except Exception as e:
+            print(f"应用网络配置时出错: {e}")
+            
+    except Exception as e:
+        print(f"启动netplan进程出错: {e}")
+
+@app.route('/')
+def index():
+    net = read_netplan()
+    current_ip = get_current_ip()
+    print(f"当前访问IP: {request.remote_addr}, 服务器IP: {current_ip}")
+    return render_template_string(HTML_TEMPLATE, **net, port=PORT)
+
+@app.route('/api/network', methods=['POST'])
+def set_network():
+    try:
+        data = request.json
+        ip = data.get("ip", "").strip()
+        mask = data.get("mask", "").strip()
+        gateway = data.get("gateway", "").strip()
+        dns = data.get("dns", "").strip()
+        
+        # 验证IP地址格式
+        if not is_valid_ip(ip):
+            return jsonify({
+                "success": False,
+                "status": "IP地址格式不正确"
+            }), 400
+            
+        if not is_valid_ip(gateway):
+            return jsonify({
+                "success": False,
+                "status": "网关地址格式不正确"
+            }), 400
+            
+        if dns and not is_valid_ip(dns):
+            return jsonify({
+                "success": False,
+                "status": "DNS地址格式不正确"
+            }), 400
+            
+        # 基础验证
+        if not all([ip, mask, gateway, dns]):
+            return jsonify({
+                "success": False,
+                "status": "所有字段都必须填写"
+            }), 400
+        
+        # 读取并更新配置
+        with open(NETPLAN_FILE) as f:
+            config = yaml.safe_load(f)
+        
+        # 更新网络配置
+        eth = config["network"]["ethernets"]["eth0"]
+        eth["dhcp4"] = False
+        eth["addresses"] = [f"{ip}/{mask}"]
+        eth["routes"] = [{"to": "default", "via": gateway}]
+        eth["nameservers"] = {"addresses": [dns]}
+        
+        # 保存配置文件
+        with open(NETPLAN_FILE, "w") as f:
+            yaml.safe_dump(config, f, default_flow_style=False)
+        
+        print(f"配置文件已保存,新IP: {ip}")
+        
+        # 在后台异步应用网络配置
+        threading.Thread(target=apply_netplan_async, args=(ip,), daemon=True).start()
+        
+        return jsonify({
+            "success": True,
+            "status": f"配置已保存并正在应用!\n\n请在20秒内按Enter键确认,否则配置会自动回滚。\n跳转地址: http://{ip}:{PORT}",
+            "new_ip": ip,
+            "port": PORT,
+            "redirect_url": f"http://{ip}:{PORT}"
+        })
+        
+    except PermissionError:
+        return jsonify({
+            "success": False,
+            "status": "权限不足,请使用sudo运行此程序"
+        }), 403
+    except FileNotFoundError:
+        return jsonify({
+            "success": False,
+            "status": f"配置文件 {NETPLAN_FILE} 不存在"
+        }), 404
+    except Exception as e:
+        return jsonify({
+            "success": False,
+            "status": f"保存失败: {str(e)}"
+        }), 500
+
+@app.route('/check_connection')
+def check_connection():
+    """检查连接状态"""
+    return jsonify({
+        "status": "connected", 
+        "message": "连接成功",
+        "server_ip": get_current_ip(),
+        "port": PORT
+    })
+
+def main():
+    """主函数"""
+    # 检查权限
+    if os.geteuid() != 0:
+        print("警告:请使用sudo运行此程序以获取必要的权限")
+        return
+    
+    current_ip = get_current_ip()
+    
+    print("=" * 60)
+    print("网络配置服务启动")
+    print("=" * 60)
+    print(f"当前服务器IP: {current_ip}")
+    print(f"服务端口: {PORT}")
+    print(f"访问地址: http://{current_ip}:{PORT}")
+    print("=" * 60)
+    print("保存配置后,会自动跳转到新的IP地址")
+    print("注意:请确保防火墙允许端口访问")
+    print("=" * 60)
+    
+    # 检查防火墙设置
+    try:
+        result = subprocess.run(["ufw", "status"], capture_output=True, text=True)
+        if "Status: active" in result.stdout:
+            print("检测到UFW防火墙,请确保端口已开放:")
+            print(f"  sudo ufw allow {PORT}")
+            print(f"  sudo ufw allow 22  # SSH端口(可选)")
+    except:
+        pass
+    
+    # 使用gevent WSGI服务器
+    server = pywsgi.WSGIServer(('0.0.0.0', PORT), app)
+    print(f"服务器已启动,监听端口: {PORT}")
+    print("按 Ctrl+C 停止服务")
+    
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        print("\n服务已停止")
+    except Exception as e:
+        print(f"服务器错误: {e}")
+
+if __name__ == "__main__":
+    main()

+ 9 - 0
services/ipset/install.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+mkdir -p /usr/local/ipset/bin
+cp -f ./bin/ipset /usr/local/ipset/bin
+cp -f ./run.sh /usr/local/ipset
+cp -f ./ipset.service /usr/lib/systemd/system
+
+systemctl daemon-reload
+systemctl enable --now ipset

+ 20 - 0
services/ipset/ipset.service

@@ -0,0 +1,20 @@
+[Unit]
+Description=IP SET
+After=network.target
+Wants=network.target
+
+[Service]
+Type=simple
+User=root
+Group=root
+WorkingDirectory=/usr/local/ipset
+ExecStart=bash run.sh
+Restart=always
+RestartSec=3
+
+# 如果需要日志
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target

+ 38 - 0
services/ipset/ipset.spec

@@ -0,0 +1,38 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+    ['app.py'],
+    pathex=[],
+    binaries=[],
+    datas=[],
+    hiddenimports=[],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    noarchive=False,
+    optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    a.binaries,
+    a.datas,
+    [],
+    name='ipset',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    runtime_tmpdir=None,
+    console=True,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+)

+ 5 - 0
services/ipset/run.sh

@@ -0,0 +1,5 @@
+#/bin/bash
+
+# start service
+chmod +x /usr/local/ipset/bin/ipset
+/usr/local/ipset/bin/ipset

+ 6 - 0
services/ipset/uninstall.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+systemctl disable --now ipset.service
+rm -r /usr/local/ipset
+rm /usr/lib/systemd/system/ipset.service
+systemctl daemon-reload