├── .gitignore ├── README.md └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npstart 2 | 3 | Ubuntu20.04+ 一键编译 Naiveproxy 4 | 5 | ## Quick Start 6 | 7 | ```shell 8 | wget -qO /home/npstart.py https://raw.githubusercontent.com/QIN2DIM/np-start/main/main.py && python3 /home/npstart.py 9 | ``` 10 | 11 | 之后可通过快捷指令 `npstart` 运行脚本。 12 | 13 | ## Features 14 | 15 | - [x] 自动编译最新版 Naiveproxy,注册系统服务,脚手架模式管理系统服务 16 | - [x] 自动配置 BBR+FQ 拥塞控制策略 17 | - [x] 优化 HTTP/2 网络环境,大幅度提升吞吐量,降低延时 18 | - [ ] 监听、拉取、更新代理核心 19 | 20 | ## Reference 21 | 22 | [Run Caddy as a daemon · klzgrad/naiveproxy Wiki](https://github.com/klzgrad/naiveproxy/wiki/Run-Caddy-as-a-daemon) 23 | 24 | [caddyserver/xcaddy: Build Caddy with plugins](https://github.com/caddyserver/xcaddy) 25 | 26 | [Caddyfile Concepts — Caddy Documentation](https://caddyserver.com/docs/caddyfile/concepts#structure) 27 | 28 | [Optimizing HTTP/2 prioritization with BBR and tcp_notsent_lowat](https://blog.cloudflare.com/http-2-prioritization-with-nginx/) 29 | 30 | [Optimizing TCP for high WAN throughput while preserving low latency | Noise](https://noise.getoto.net/2022/07/01/optimizing-tcp-for-high-wan-throughput-while-preserving-low-latency/) 31 | 32 | [ip-sysctl.txt - Linux kernel source tree](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/networking/ip-sysctl.txt?id=bb077d600689dbf9305758efed1e16775db1c84c#n843) 33 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Time : 2022/10/9 13:06 3 | # Author : QIN2DIM 4 | # Github : https://github.com/QIN2DIM 5 | # Description: 6 | import base64 7 | import functools 8 | import json 9 | import logging 10 | import os 11 | import re 12 | import sys 13 | import time 14 | import typing 15 | import uuid 16 | from dataclasses import dataclass 17 | 18 | # 阻止 python2 及非 linux 系统运行 19 | if sys.version_info[0] < 3 or sys.platform != "linux": 20 | sys.exit() 21 | os.system("clear") 22 | 23 | logging.basicConfig( 24 | level=logging.INFO, 25 | format="%(asctime)s [%(levelname)s] %(message)s", 26 | datefmt="%Y/%m/%d %H:%M:%S", 27 | ) 28 | 29 | WORKSPACE = "/home/naiveproxy/" 30 | PATH_CADDY = os.path.join(WORKSPACE, "caddy") 31 | PATH_CADDYFILE = os.path.join(WORKSPACE, "Caddyfile") 32 | LOCAL_SCRIPT = "/home/npstart.py" 33 | REMOTE_GITHUB = "https://raw.githubusercontent.com/QIN2DIM/np-start/main/main.py" 34 | 35 | CADDYFILE_TEMPLATE = """ 36 | :443, [domain] 37 | tls [email] 38 | route { 39 | forward_proxy { 40 | basic_auth [username] [password] 41 | hide_ip 42 | hide_via 43 | probe_resistance 44 | } 45 | reverse_proxy https://demo.cloudreve.org { 46 | header_up Host {upstream_hostport} 47 | header_up X-Forwarded-Host {host} 48 | } 49 | } 50 | """ 51 | 52 | V2RAYN_TEMPLATE = """ 53 | { 54 | "listen": "socks://127.0.0.1:58185", 55 | "proxy": "[proxy]" 56 | } 57 | """ 58 | 59 | GUIDER_PANEL = """ ------------------------------------------- 60 | |********** npstart **********| 61 | |********** Author: QIN2DIM **********| 62 | |********** Version: 0.1.2 **********| 63 | ------------------------------------------- 64 | Tips: npstart 命令再次运行本脚本. 65 | ............................................. 66 | 67 | ############################### 68 | 69 | ..................... 70 | 1) 敏捷部署 Naiveproxy 71 | 2) 卸载 72 | ..................... 73 | 3) 启动 74 | 4) 暂停 75 | 5) 重载 76 | 6) 运行状态 77 | ..................... 78 | 7) 查看当前配置 79 | 8) 重新配置 80 | ..................... 81 | 9) 更新 npstart 82 | 83 | ############################### 84 | 85 | 86 | 87 | 0)退出 88 | ............................................. 89 | 请选择: """ 90 | 91 | NAIVEPROXY_SERVICE = f""" 92 | [Unit] 93 | Description=npstart:Caddy2 web server with naiveproxy plugin 94 | Documentation=https://github.com/QIN2DIM/np-start 95 | After=network.target network-online.target 96 | Requires=network-online.target 97 | 98 | [Service] 99 | # User=naiveproxy 100 | # Group=naiveproxy 101 | ExecStart={PATH_CADDY} run --environ --config {PATH_CADDYFILE} 102 | ExecReload={PATH_CADDY} reload --config {PATH_CADDYFILE} 103 | TimeoutStopSec=5s 104 | LimitNOFILE=1048576 105 | LimitNPROC=512 106 | PrivateTmp=true 107 | ProtectSystem=full 108 | AmbientCapabilities=CAP_NET_BIND_SERVICE 109 | # Restart=always 110 | # RestartSec=45s 111 | 112 | [Install] 113 | WantedBy=multi-user.target 114 | """ 115 | 116 | SHELL_NPSTART = f""" 117 | if [ ! -f "{LOCAL_SCRIPT}" ]; then 118 | echo "Local script is missing, trying to sync upstream content" 119 | wget -qO {LOCAL_SCRIPT} {REMOTE_GITHUB} 120 | fi 121 | python3 {LOCAL_SCRIPT} 122 | """ 123 | 124 | 125 | @dataclass 126 | class CaddyServer: 127 | username: str = f"{uuid.uuid4().hex}"[:8] 128 | password: str = uuid.uuid4().hex 129 | email: str = f"{str(uuid.uuid4().hex)[:8]}@tesla.com" 130 | domain: str = "" 131 | port: str = "443" 132 | scheme: str = "https" # "https" or "quic" 133 | 134 | def get_caddyfile(self): 135 | caddyfile = CADDYFILE_TEMPLATE.strip() 136 | p2v = { 137 | "[domain]": self.domain, 138 | "[email]": self.email, 139 | "[username]": self.username, 140 | "[password]": self.password, 141 | } 142 | for placeholder in p2v: 143 | caddyfile = caddyfile.replace(placeholder, p2v[placeholder]) 144 | return caddyfile 145 | 146 | def get_v2rayn_custom_server(self): 147 | v2rayn_custom_server = V2RAYN_TEMPLATE.strip() 148 | proxy = f"{self.scheme}://{self.username}:{self.password}@{self.domain}" 149 | return v2rayn_custom_server.replace("[proxy]", proxy) 150 | 151 | def get_nekoray_sharelink(self): 152 | typecode = f"{self.username}:{self.password}@{self.domain}:{self.port}#{self.domain}" 153 | sharelink = f"naive+{self.scheme}://{typecode}" 154 | return sharelink 155 | 156 | def get_shadowrocket_sharelink(self): 157 | typecode = f"{self.username}:{self.password}@{self.domain}:{self.port}" 158 | sharelink = f'http2://{base64.b64encode(typecode.encode("utf8")).decode("utf8")}' 159 | return sharelink.replace("=", "") 160 | 161 | 162 | @dataclass 163 | class ClientSettings: 164 | dir_workspace: str = WORKSPACE 165 | path_caddyfile: str = PATH_CADDYFILE 166 | 167 | path_config_server: typing.Optional[str] = "" 168 | path_client_config: typing.Optional[str] = "" 169 | caddy: typing.Optional[CaddyServer] = None 170 | 171 | def __post_init__(self): 172 | self.path_config_server = os.path.join(self.dir_workspace, "caddy_server.json") 173 | self.path_client_config = os.path.join(self.dir_workspace, "clients.json") 174 | self.caddy = self._preload_config() 175 | 176 | def _preload_config(self) -> CaddyServer: 177 | os.makedirs(self.dir_workspace, exist_ok=True) 178 | try: 179 | with open(self.path_config_server, "r", encoding="utf8") as file: 180 | return CaddyServer(**json.load(file)) 181 | except (FileNotFoundError, KeyError, TypeError): 182 | return CaddyServer() 183 | 184 | def refresh_localcache(self, drop: bool = False): 185 | """修改 Caddyfile 以及客户端配置""" 186 | localcache = { 187 | "v2rayn_custom_server": self.caddy.get_v2rayn_custom_server(), 188 | "nekoray_sharelink": self.caddy.get_nekoray_sharelink(), 189 | "shadowrocket_sharelink": self.caddy.get_shadowrocket_sharelink(), 190 | } 191 | with open(self.path_client_config, "w", encoding="utf8") as file: 192 | json.dump(localcache, file) 193 | with open(self.path_config_server, "w", encoding="utf8") as file: 194 | json.dump(self.caddy.__dict__, file, indent=4) 195 | with open(self.path_caddyfile, "w", encoding="utf8") as file: 196 | file.write(self.caddy.get_caddyfile()) 197 | 198 | if drop: 199 | print(" ↓ ↓ V2RayN ↓ ↓ ".center(50, "=")) 200 | print(localcache.get("v2rayn_custom_server")) 201 | print(" ↓ ↓ NekoRay & Matsuri ↓ ↓ ".center(50, "=")) 202 | print(f"\n{localcache.get('nekoray_sharelink')}\n") 203 | print(" ↓ ↓ Shadowrocket ↓ ↓ ".center(50, "=")) 204 | print(f"\n{localcache.get('shadowrocket_sharelink')}\n") 205 | 206 | 207 | def check_caddy(func): 208 | @functools.wraps(func) 209 | def wrapped(*args, **kwargs): 210 | if not os.path.isfile(PATH_CADDY) or not os.path.getsize(PATH_CADDY): 211 | logging.error(f"Naiveproxy 未初始化,請先執行「敏捷部署」 - func={func.__name__}") 212 | else: 213 | return func(*args, **kwargs) 214 | 215 | return wrapped 216 | 217 | 218 | def skip_recompile(func): 219 | @functools.wraps(func) 220 | def wrapped(*args, **kwargs): 221 | if os.path.isfile(PATH_CADDY) and os.path.getsize(PATH_CADDY): 222 | logging.error(f"Naiveproxy 已编译,如需修改参数请执行「重新配置」 - func={func.__name__}") 223 | else: 224 | return func(*args, **kwargs) 225 | 226 | return wrapped 227 | 228 | 229 | class CaddyService: 230 | NAME: str = "naiveproxy" 231 | 232 | def __init__(self): 233 | self.path_units = f"/etc/systemd/system/{self.NAME}.service" 234 | 235 | self._on_service() 236 | 237 | def _on_service(self): 238 | if not os.path.isfile(self.path_units): 239 | with open(self.path_units, "w", encoding="utf8") as file: 240 | file.write(NAIVEPROXY_SERVICE) 241 | os.system("systemctl daemon-reload") 242 | os.system(f"systemctl enable {self.NAME} >/dev/null 2>&1") 243 | 244 | @check_caddy 245 | def caddy_start(self): 246 | """后台运行 CaddyServer""" 247 | os.system(f"systemctl start {self.NAME}") 248 | logging.info("Start the naiveproxy") 249 | 250 | @check_caddy 251 | def caddy_stop(self): 252 | """停止 CaddyServer""" 253 | os.system(f"systemctl stop {self.NAME}") 254 | logging.info("Stop the naiveproxy") 255 | 256 | @check_caddy 257 | def caddy_reload(self): 258 | """重启 CaddyServer 重新读入配置""" 259 | os.system(f"systemctl reload-or-restart {self.NAME}") 260 | logging.info("Reload the naiveproxy") 261 | 262 | @check_caddy 263 | def caddy_status(self): 264 | """查看 CaddyServer 运行状态""" 265 | os.system(f"systemctl status {self.NAME}") 266 | 267 | def remove(self): 268 | os.system(f"systemctl stop {self.NAME}") 269 | os.system(f"systemctl disable {self.NAME} >/dev/null 2>&1") 270 | os.system(f"rm {self.path_units}") 271 | os.system("systemctl daemon-reload") 272 | logging.info("Remove the naiveproxy") 273 | 274 | 275 | class Alias: 276 | BIN_NAME: str = "npstart" 277 | 278 | def register(self): 279 | for path_bin in [f"/usr/bin/{self.BIN_NAME}", f"/usr/sbin/{self.BIN_NAME}"]: # unnecessary 280 | if not os.path.isfile(path_bin): 281 | with open(path_bin, "w", encoding="utf8") as file: 282 | file.write(SHELL_NPSTART) 283 | os.system(f"chmod +x {path_bin}") 284 | 285 | def remove(self): 286 | os.system(f"rm /usr/bin/{self.BIN_NAME}") 287 | os.system(f"rm /usr/sbin/{self.BIN_NAME}") 288 | 289 | 290 | class CMDPanel: 291 | def __init__(self): 292 | self.path_caddy = PATH_CADDY 293 | self.csm = ClientSettings() 294 | self.caddy = self.csm.caddy 295 | self.utils = CaddyService() 296 | 297 | self.alias = Alias() 298 | self.alias.register() 299 | 300 | @staticmethod 301 | def _optimize(): 302 | """Network FineTune""" 303 | logging.info("Naiveproxy Network Performance Tuning") 304 | cmd_queue = ( 305 | # ENABLE BBR+FQ CONGESTION CONTROL ALGORITHM 306 | "sudo sysctl -w net.core.default_qdisc=fq" 307 | "sudo sysctl -w net.ipv4.tcp_congestion_control=bbr", 308 | # OPTIMIZING TCP FOR HIGH WAN THROUGHPUT WHILE PRESERVING LOW LATENCY 309 | # "sudo sysctl -w net.ipv4.tcp_slow_start_after_idle=0", 310 | # "sudo sysctl -w net.ipv4.tcp_rmem='8192 262144 536870912'", 311 | # "sudo sysctl -w net.ipv4.tcp_wmem='4096 16384 536870912'", 312 | # "sudo sysctl -w net.ipv4.tcp_adv_win_scale=-2", 313 | # "sudo sysctl -w net.ipv4.tcp_collapse_max_bytes=6291456", 314 | # "sudo sysctl -w net.ipv4.tcp_notsent_lowat=131072", 315 | ) 316 | for cmd in cmd_queue: 317 | os.system(cmd) 318 | 319 | def _compile(self): 320 | # ==================== preprocess ==================== 321 | os.system("clear") 322 | logging.info("Check snap, wget, port80 and port443") 323 | cmd_queue = ("apt install -y snapd wget >/dev/null 2>&1", "nginx -s stop >/dev/null 2>&1") 324 | for cmd in cmd_queue: 325 | os.system(cmd) 326 | # ==================== handle server ==================== 327 | logging.info("Check go1.18+") 328 | os.system("apt remove golang-go -y >/dev/null 2>&1") 329 | os.system("snap install go --classic >/dev/null 2>&1") 330 | 331 | logging.info("Check xcaddy") 332 | cmd_queue = ( 333 | "wget https://github.com/caddyserver/xcaddy/releases/download/v0.3.1/xcaddy_0.3.1_linux_amd64.deb >/dev/null 2>&1", 334 | "apt install -y ./xcaddy_0.3.1_linux_amd64.deb >/dev/null 2>&1", 335 | "rm xcaddy_0.3.1_linux_amd64.deb", 336 | ) 337 | for cmd in cmd_queue: 338 | os.system(cmd) 339 | 340 | os.makedirs(os.path.dirname(self.path_caddy), exist_ok=True) 341 | if not os.path.isfile(self.path_caddy): 342 | logging.info("Build caddy with naiveproxy") 343 | os.system( 344 | f"export PATH=$PATH:/snap/bin && xcaddy build " 345 | f"--output {self.path_caddy} " 346 | f"--with github.com/caddyserver/forwardproxy@caddy2=github.com/klzgrad/forwardproxy@naive" 347 | ) 348 | else: 349 | logging.info("Caddy already exists, skip compilation") 350 | logging.info(self.path_caddy) 351 | 352 | @staticmethod 353 | def _guide_domain(prompt: str): 354 | pattern = re.compile(r"(?:\w(?:[\w\-]{0,61}\w)?\.)+[a-zA-Z]{2,6}") 355 | while True: 356 | domain = input(prompt).strip() 357 | result = re.findall(pattern, domain) 358 | if result and result.__len__() == 1: 359 | return result[0] 360 | 361 | @skip_recompile 362 | def deploy(self): 363 | self.caddy.username = input(">> 输入用户名[username](回车随机配置):").strip() or self.caddy.username 364 | self.caddy.password = input(">> 输入密码[password](回车随机配置):").strip() or self.caddy.password 365 | self.caddy.domain = self._guide_domain(prompt=">> 输入解析到本机Ipv4的域名[domain]:") 366 | self._compile() 367 | 368 | if not os.path.isfile(self.path_caddy): 369 | logging.error("👻 編譯失敗") 370 | else: 371 | self._optimize() # deploy 372 | logging.info("🎉 编译成功! 按任意键部署 Naiveproxy 系统服务") 373 | input() 374 | self.csm.refresh_localcache(drop=True) # deploy 375 | self.utils.caddy_start() 376 | 377 | @check_caddy 378 | def delete(self): 379 | """删除 np-start 缓存""" 380 | if input(">> 卸载「已编译的Caddy服務及缓存數據」[y/n] ").strip().lower().startswith("y"): 381 | self.utils.remove() 382 | self.alias.remove() 383 | os.system(f"rm -rf {WORKSPACE} >/dev/null 2>&1") 384 | logging.info("Delete cache of the naiveproxy") 385 | else: 386 | logging.info(f"Withdraw operation") 387 | 388 | @check_caddy 389 | def checkout(self): 390 | """查看客户端配置信息""" 391 | self.csm.refresh_localcache(drop=True) # check 392 | 393 | @check_caddy 394 | def reset(self): 395 | if input(">> 是否使用上次配置的用戶名?[y/n] ").strip().lower().startswith("n"): 396 | self.caddy.username = input(">> 输入用户名[username](回车随机配置):").strip() 397 | if input(">> 是否使用上次配置的密碼?[y/n] ").strip().lower().startswith("n"): 398 | self.caddy.password = input(">> 输入密码[password](回车随机配置):").strip() 399 | if input(f">> 是否使用上次配置的域名({self.caddy.domain})?[y/n] ").strip().lower().startswith("n"): 400 | self.caddy.domain = self._guide_domain(prompt=">> 输入解析到本机Ipv4的域名[domain]:") 401 | self.csm.refresh_localcache() # reset 402 | logging.info("reset user config") 403 | self.utils.caddy_reload() 404 | 405 | def upgrade(self): 406 | # TODO checkout branch version 407 | logging.info("Updating script ...") 408 | time.sleep(1) 409 | bak = f"{LOCAL_SCRIPT}.bak" 410 | os.system(f"wget -qO {bak} {REMOTE_GITHUB}") 411 | if os.path.isfile(bak) and os.path.getsize(bak): 412 | os.system(f"mv {bak} {LOCAL_SCRIPT}") 413 | os.system(self.alias.BIN_NAME) 414 | 415 | def startup(self): 416 | if not (item := input(GUIDER_PANEL).strip()): 417 | return 418 | 419 | if item == "1": 420 | self.deploy() 421 | elif item == "2": 422 | self.delete() 423 | elif item == "3": 424 | self.utils.caddy_start() 425 | elif item == "4": 426 | self.utils.caddy_stop() 427 | elif item == "5": 428 | self.utils.caddy_reload() 429 | self._optimize() # caddy reload 430 | elif item == "6": 431 | self.utils.caddy_status() 432 | elif item == "7": 433 | self.checkout() 434 | elif item == "8": 435 | self.reset() 436 | elif item == "9": 437 | self.upgrade() 438 | elif item == " 10": # sync upstream 439 | logging.info("NotImplement") 440 | 441 | 442 | if __name__ == "__main__": 443 | try: 444 | CMDPanel().startup() 445 | except KeyboardInterrupt: 446 | print("\n") 447 | 448 | # wget -qO /home/npstart.py https://raw.githubusercontent.com/QIN2DIM/np-start/dev/main.py && python3 /home/npstart.py 449 | --------------------------------------------------------------------------------