From a7fb8728c5e12d948ea06e4cccc732fa79ab4b17 Mon Sep 17 00:00:00 2001 From: laoxinH Date: Sun, 1 Dec 2024 17:50:04 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Cloudflare-Origin=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=9F=E5=90=8D=E9=9A=90=E8=97=8F=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=E8=AE=BF=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- natter-docker/README.md | 7 + natter-docker/cloudflare-origin/README.md | 44 +++++ natter-docker/cloudflare-origin/cf-origin.py | 186 ++++++++++++++++++ .../cloudflare-origin/docker-compose.yml | 11 ++ 4 files changed, 248 insertions(+) create mode 100644 natter-docker/cloudflare-origin/README.md create mode 100644 natter-docker/cloudflare-origin/cf-origin.py create mode 100644 natter-docker/cloudflare-origin/docker-compose.yml diff --git a/natter-docker/README.md b/natter-docker/README.md index 6ecf61a..85f910f 100644 --- a/natter-docker/README.md +++ b/natter-docker/README.md @@ -104,3 +104,10 @@ BT 类程序的特点是,需要向 Tracker 宣告自己的端口号。 订阅服务本身由 HTTP 跳转实现。 本仓库提供了以下用例,需要填写您的 CloudFlare API 令牌: - [V2Fly-Nginx-CloudFlare](v2fly-nginx-cloudflare) + +### 隐藏域名端口号 + +利用隐藏端口号访问域名,实现不加端口号访问域名。 +隐藏域名端口号通过 CloudFlare 的回源规则(origin-rules)实现。 +本仓库提供了以下用例,需要填写您的 CloudFlare API 令牌: +- [Cloudflare-Origin](cloudflare-origin) \ No newline at end of file diff --git a/natter-docker/cloudflare-origin/README.md b/natter-docker/cloudflare-origin/README.md new file mode 100644 index 0000000..a242a5c --- /dev/null +++ b/natter-docker/cloudflare-origin/README.md @@ -0,0 +1,44 @@ +# Cloudflare-origin + +此目录为在 Docker 中使用 Natter 的一个示例。 + +本示例可以运行 Natter 容器将指定端口映射至公网并解析到 cloudflare,同时使用 CloudFlare 的回源规则(origin-rules)实现域名隐藏端口号访问。 + + +## 使用前 + +- 您的域名需已加入 CloudFlare + +- 修改 `cf-origin.py` 中的相关参数: + - `cf_domains` 修改为实现隐藏端口号的域名。 + - `cf_origin_rule_descriptions` 回源规则的描述, 英文字符。 + - `cf_origin_rule_expressions` 回源规则表达式, 不清楚如何编写可以通过 cf 控制台的origin-rules编辑页面建立规则后复制过来。 + - `cf_auth_email` 值修改为您的 CloudFlare 邮箱。 + - `cf_auth_key` 值修改为您的 CloudFlare API Key。获取方式: + - 登录 [CloudFlare](https://dash.cloudflare.com/) + - 进入 [https://dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens) + - 点击 **Global API Key** 右侧「查看」按钮 +- `cf_domains` `cf_origin_rule_descriptions` `cf_origin_rule_expressions` 可以添加多条参数 +- 使用 `cd` 命令进入此目录 + +## 开始使用 + +前台运行: +```bash +docker compose up +``` + +后台运行: +```bash +docker compose up -d +``` + +查看日志: +```bash +docker compose logs -f +``` + +结束运行: +```bash +docker compose down +``` diff --git a/natter-docker/cloudflare-origin/cf-origin.py b/natter-docker/cloudflare-origin/cf-origin.py new file mode 100644 index 0000000..c631b15 --- /dev/null +++ b/natter-docker/cloudflare-origin/cf-origin.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +import urllib.request +import json +import sys + +# Natter notification script arguments +protocol, private_ip, private_port, public_ip, public_port = sys.argv[1:6] + +cf_domains = ["origin.example.com"] +cf_origin_rule_descriptions = ["nas"] +cf_origin_rule_expressions = ['(http.host contains "origin.example.com")'] +cf_auth_email = "laoxinhxx@gmail.com" +cf_auth_key = "3eb7ba3c36bbf34e0add81da0e84980166641" + +def main(): + cf = CloudFlareDNS(cf_auth_email, cf_auth_key) + for i in range(len(cf_domains)): + print(f"Setting {cf_domains[i]} A record to {public_ip}...") + cf.set_a_record(cf_domains[i], public_ip) + for i in range(len(cf_origin_rule_descriptions)): + print( + f"Setting {cf_domains[i]} {cf_origin_rule_expressions[i]} origin rule to {cf_origin_rule_expressions[i]} port {public_port}...") + cf.set_origin_rule(cf_domains[i], cf_origin_rule_descriptions[i], cf_origin_rule_expressions[i], int(public_port)) + + +class CloudFlareDNS: + def __init__(self, auth_email, auth_key): + self.opener = urllib.request.build_opener() + self.opener.addheaders = [ + ("X-Auth-Email", auth_email), + ("X-Auth-Key", auth_key), + ("Content-Type", "application/json") + ] + + def set_a_record(self, name, ipaddr): + zone_id = self._find_zone_id(name) + if not zone_id: + raise ValueError("%s is not on CloudFlare" % name) + rec_id = self._find_a_record(zone_id, name) + if not rec_id: + rec_id = self._create_a_record(zone_id, name, ipaddr) + else: + rec_id = self._update_a_record(zone_id, rec_id, name, ipaddr) + return rec_id + + def set_origin_rule(self, name, description, expression, port): + zone_id = self._find_zone_id(name) + if not zone_id: + raise ValueError("%s is not on CloudFlare" % name) + ruleset_id, rule_id = self._get_origin_ruleset_id_and_rule_id(name, description) + if not rule_id: + rule_id = self._create_origin_rule(name, description, expression, port) + else: + rule_id = self._update_origin_rule(name, description, expression, port) + return rule_id + + def _url_req(self, url, data=None, method=None): + data_bin = None + if data is not None: + data_bin = json.dumps(data).encode() + req = urllib.request.Request(url, data=data_bin, method=method) + try: + with self.opener.open(req, timeout=10) as res: + ret = json.load(res) + except urllib.error.HTTPError as e: + print(e) + ret = json.load(e) + if "errors" not in ret: + raise RuntimeError(ret) + if not ret.get("success"): + raise RuntimeError(ret["errors"]) + return ret + + def _find_zone_id(self, name): + name = name.lower() + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones" + ) + # print(data) + for zone_data in data["result"]: + zone_name = zone_data["name"] + if name == zone_name or name.endswith("." + zone_name): + zone_id = zone_data["id"] + return zone_id + return None + + def _find_a_record(self, zone_id, name): + name = name.lower() + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" + ) + for rec_data in data["result"]: + if rec_data["type"] == "A" and rec_data["name"] == name: + rec_id = rec_data["id"] + return rec_id + return None + + def _create_a_record(self, zone_id, name, ipaddr, proxied=True, ttl=120): + name = name.lower() + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", + data={ + "content": ipaddr, + "name": name, + "proxied": proxied, + "type": "A", + "ttl": ttl + }, + method="POST" + ) + return data["result"]["id"] + + def _update_a_record(self, zone_id, rec_id, name, ipaddr, proxied=True, ttl=120): + name = name.lower() + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}", + data={ + "content": ipaddr, + "name": name, + "proxied": proxied, + "type": "A", + "ttl": ttl + }, + method="PUT" + ) + return data["result"]["id"] + + def _get_origin_ruleset_id_and_rule_id(self, name, description): + zone_id = self._find_zone_id(name) + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/46f350f8845962e5bbed5d98f613e617/rulesets/phases/http_request_origin/entrypoint" + ) + for rule_data in data["result"]["rules"]: + if rule_data["description"] == description: + rule_id = rule_data["id"] + return data["result"]["id"], rule_id + return data["result"]["id"], None + + def _create_origin_rule(self, name, description, expression, port): + zone_id = self._find_zone_id(name) + ruleset_id, rule_id = self._get_origin_ruleset_id_and_rule_id(name, description) + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/{ruleset_id}/rules", + { + "description": description, + "expression": expression, + "action": "route", + # "action_parameters": { + # "id": "4814384a9e5d4991b9815dcfc25d2f1f" + # }, + "ref": "my_ref", + "action_parameters": { + 'origin': {'port': port} + } + }, + method="POST" + ) + return data["result"]["id"] + + def _update_origin_rule(self, name, description, expression, port): + zone_id = self._find_zone_id(name) + ruleset_id, rule_id = self._get_origin_ruleset_id_and_rule_id(name, description) + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id}", + { + "description": description, + "expression": '(http.host contains "modwu.com")', + "action": "route", + # "action_parameters": { + # "id": "4814384a9e5d4991b9815dcfc25d2f1f" + # }, + # "ref": "my_ref", + "action_parameters": { + 'origin': {'port': port} + } + }, + method="PATCH" + ) + return data["result"]["id"] + + +# 示例用法 + + +if __name__ == "__main__": + main() diff --git a/natter-docker/cloudflare-origin/docker-compose.yml b/natter-docker/cloudflare-origin/docker-compose.yml new file mode 100644 index 0000000..739c6b6 --- /dev/null +++ b/natter-docker/cloudflare-origin/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3" +services: + natter-mc: + command: -e /opt/cf-origin.py -r + volumes: + - ./cf-origin.py:/opt/cf-origin.py + environment: + - TZ=Asia/Shanghai + network_mode: host + image: nattertool/natter + restart: always From 51f3f6842bcea6ec36055f32bc6f202a1ac7a91c Mon Sep 17 00:00:00 2001 From: laoxinH Date: Sun, 1 Dec 2024 17:59:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Cloudflare-Origin=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=9F=E5=90=8D=E9=9A=90=E8=97=8F=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=E8=AE=BF=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- natter-docker/cloudflare-origin/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-docker/cloudflare-origin/docker-compose.yml b/natter-docker/cloudflare-origin/docker-compose.yml index 739c6b6..7a660f0 100644 --- a/natter-docker/cloudflare-origin/docker-compose.yml +++ b/natter-docker/cloudflare-origin/docker-compose.yml @@ -1,6 +1,6 @@ version: "3" services: - natter-mc: + natter-cf-origin: command: -e /opt/cf-origin.py -r volumes: - ./cf-origin.py:/opt/cf-origin.py From 4234a7cd90c5975addff6457f5304d246fb2b4b7 Mon Sep 17 00:00:00 2001 From: laoxinH Date: Mon, 2 Dec 2024 19:20:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=94=B9=20cf-api=20=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- natter-docker/cloudflare-origin/README.md | 13 +++++++++++++ natter-docker/cloudflare-origin/cf-origin.py | 5 +---- natter-docker/cloudflare-origin/docker-compose.yml | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/natter-docker/cloudflare-origin/README.md b/natter-docker/cloudflare-origin/README.md index a242a5c..66125e6 100644 --- a/natter-docker/cloudflare-origin/README.md +++ b/natter-docker/cloudflare-origin/README.md @@ -42,3 +42,16 @@ docker compose logs -f ```bash docker compose down ``` + + +## 修改参数 + +### 需要暴露的端口号 + +修改 `natter-cf-origin:` 部分: + +```yaml +command: -e /opt/cf-origin.py -p 80 -r +``` + +将 `80` 修改为需要对外访问的端口。 \ No newline at end of file diff --git a/natter-docker/cloudflare-origin/cf-origin.py b/natter-docker/cloudflare-origin/cf-origin.py index c631b15..3e56403 100644 --- a/natter-docker/cloudflare-origin/cf-origin.py +++ b/natter-docker/cloudflare-origin/cf-origin.py @@ -128,7 +128,7 @@ def _update_a_record(self, zone_id, rec_id, name, ipaddr, proxied=True, ttl=120) def _get_origin_ruleset_id_and_rule_id(self, name, description): zone_id = self._find_zone_id(name) data = self._url_req( - f"https://api.cloudflare.com/client/v4/zones/46f350f8845962e5bbed5d98f613e617/rulesets/phases/http_request_origin/entrypoint" + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/phases/http_request_origin/entrypoint" ) for rule_data in data["result"]["rules"]: if rule_data["description"] == description: @@ -145,9 +145,6 @@ def _create_origin_rule(self, name, description, expression, port): "description": description, "expression": expression, "action": "route", - # "action_parameters": { - # "id": "4814384a9e5d4991b9815dcfc25d2f1f" - # }, "ref": "my_ref", "action_parameters": { 'origin': {'port': port} diff --git a/natter-docker/cloudflare-origin/docker-compose.yml b/natter-docker/cloudflare-origin/docker-compose.yml index 7a660f0..8778dc0 100644 --- a/natter-docker/cloudflare-origin/docker-compose.yml +++ b/natter-docker/cloudflare-origin/docker-compose.yml @@ -1,7 +1,7 @@ version: "3" services: natter-cf-origin: - command: -e /opt/cf-origin.py -r + command: -e /opt/cf-origin.py -p 80 -r volumes: - ./cf-origin.py:/opt/cf-origin.py environment: