Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions natter-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,10 @@ BT 类程序的特点是,需要向 Tracker 宣告自己的端口号。
订阅服务本身由 HTTP 跳转实现。
本仓库提供了以下用例,需要填写您的 CloudFlare API 令牌:
- [V2Fly-Nginx-CloudFlare](v2fly-nginx-cloudflare)

### 隐藏域名端口号

利用隐藏端口号访问域名,实现不加端口号访问域名。
隐藏域名端口号通过 CloudFlare 的回源规则(origin-rules)实现。
本仓库提供了以下用例,需要填写您的 CloudFlare API 令牌:
- [Cloudflare-Origin](cloudflare-origin)
57 changes: 57 additions & 0 deletions natter-docker/cloudflare-origin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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
```


## 修改参数

### 需要暴露的端口号

修改 `natter-cf-origin:` 部分:

```yaml
command: -e /opt/cf-origin.py -p 80 -r
```

将 `80` 修改为需要对外访问的端口。
183 changes: 183 additions & 0 deletions natter-docker/cloudflare-origin/cf-origin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/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/{zone_id}/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",
"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()
11 changes: 11 additions & 0 deletions natter-docker/cloudflare-origin/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "3"
services:
natter-cf-origin:
command: -e /opt/cf-origin.py -p 80 -r
volumes:
- ./cf-origin.py:/opt/cf-origin.py
environment:
- TZ=Asia/Shanghai
network_mode: host
image: nattertool/natter
restart: always