From 65990cceac480922d055c3725d6ac1f305899fee Mon Sep 17 00:00:00 2001 From: jdjingdian Date: Sat, 10 Aug 2024 13:39:40 +0800 Subject: [PATCH] feat: combine cloudflare script and introduce POT port record method --- docs/cf-aio.md | 46 ++++ docs/pot.md | 125 +++++++++++ docs/script.md | 1 + scripts/cf-aio.conf | 11 + scripts/cf-aio.py | 464 +++++++++++++++++++++++++++++++++++++++++ scripts/pot/potssh.ps1 | 25 +++ scripts/pot/potssh.sh | 16 ++ 7 files changed, 688 insertions(+) create mode 100644 docs/cf-aio.md create mode 100644 docs/pot.md create mode 100644 scripts/cf-aio.conf create mode 100755 scripts/cf-aio.py create mode 100644 scripts/pot/potssh.ps1 create mode 100644 scripts/pot/potssh.sh diff --git a/docs/cf-aio.md b/docs/cf-aio.md new file mode 100644 index 0000000..8d9acb2 --- /dev/null +++ b/docs/cf-aio.md @@ -0,0 +1,46 @@ +# CloudFlare All In One 脚本使用说明 +CloudFlare All In One 脚本(以下简称aio脚本)整合了 `cf-srv.py` 和 `cf-redir.py` 脚本的能力,并加入了 POT ( Port Over TXT )的记录能力,关于 POT, 可以参考详细使用参考 [Natter POT 动态端口记录](pot.md) 。 + +aio脚本匹配natter的通知逻辑,接收5个参数,分别为 [**protocol**] [**local_ip**] [**local_port**] [**remote_ip**] [**remote_port**],其他域名、密钥等信息保存在配置中 + +aio脚本使用配置文件来决定上传的行为,***配置文件应存放在脚本的同级目录***,配置文件名固定为 **cf-aio.conf** + +```bash +scripts % ls -l +total 64 +-rw-r--r-- 1 natter staff 305 8 10 12:01 cf-aio.conf +-rwxr-xr-x 1 natter staff 17464 8 10 12:10 cf-aio.py +``` + +## 配置文件格式 +```json +{ + "cf_email":"email@example.com", + "cf_key":"d41d8cd98f00b204e9800998ecf8427e", + "direct_host":"direct.example.com", + "redirect_to_https":false, + "redirect_host":null, + "srv_host":null, + "srv_name":"_minecraft", + "pot_service_host":null, + "pot_service_key":"ssh" +} +``` + +## 配置含义 + +| 参数 | 说明 | 值类型 | 强关联参数 | +|---------------------|-----------------------------------|-------|-----------------------------------| +| ***必选项:*** | | | | +| `cf_email` | CloudFlare 登录邮箱 | 字符串 | / | +| `cf_key` | 打印此帮助并退出 | 字符串 | / | +| ***自定义选项:*** | | | | +| `direct_host` | 配置 CloudFlare 的直连域名,代表你的公网IP | 字符串或空 | / | +| `redirect_to_https` | 配置 CloudFlare 跳转时是否强制 HTTPS | 布尔值或空 | `redirect_to_host` | +| `redirect_host` | 配置 CloudFlare 跳转时是否强制 HTTPS | 字符串或空 | `direct_host`、`redirect_to_https` | +| `srv_host` | 配置 CloudFlare SRV 记录的域名 | 字符串或空 | `srv_name` | +| `srv_name` | 配置 SRV 的服务名称 | 字符串或空 | `srv_host` | +| `pot_service_host` | 配置 CloudFlare TXT记录,用于记录 POT 服务端口 | 字符串或空 | `direct_host`、`pot_service_key` | +| `pot_service_key` | 配置 POT 的服务关键字 | 字符串或空 | `direct_host`、`pot_service_host` | + +如果需要的配置有强关联参数,则强关联的参数变为必选,不可以为空 \ No newline at end of file diff --git a/docs/pot.md b/docs/pot.md new file mode 100644 index 0000000..a3ccf33 --- /dev/null +++ b/docs/pot.md @@ -0,0 +1,125 @@ +# Natter POT 动态端口记录 + +在 NAT 1 中,不仅外部 IP 是动态的,外部端口也是动态的。 虽然HTTP访问可以利用规则进行302跳转从而比较方便地访问,但是对于其他类型的协议,如FTP、SSH、RDP等,就不太方便进行访问,因此需要找到一种方法,能比较方便地了解到自己服务的端口信息。 + +在同类开源项目Natmap中,提出了 [使用NATMap在NAT-1私网IP宽带上部署SSH服务](https://github.com/heiher/natmap/wiki/ssh),其定义了一种叫IP4P的格式,将动态的公网IP和端口号以IP4P格式保存到DNS的AAAA记录里,从而可以通过DNS查询找到服务的IP和端口信息。 + +但是,使用IP4P格式保存端口信息的话,一个服务的端口号就要占用一条AAAA记录,如果有使用多个端口,会使得DNS记录里有很多AAAA记录,而这些DNS记录中指向的IP地址本质是相同的,感觉会有一点浪费资源。 + +而采用SRV记录的方式,有RFC规范,但是,一个域名记录也只能对应一个服务端口,并且本质上需要两条记录,一个A记录和一个SRV记录,如果本地需要暴露的服务端口比较多,也会创建很多域名记录。 + +为了能更方便地管理,不创建冗余的域名记录,提出使用TXT记录来保存端口信息,将这种方法叫做 POT (Port Over TXT),以 `服务关键字_协议:端口号` 的字符串格式来保存端口号信息,例如 + +```bash +ssh_tcp:12345 +ftp_tcp:34567 +rdp_tcp:23421 +``` + +这段数据会被编码成base64格式并存储在域名的txt记录里,转换后的值如`c3NoX3RjcDoxMjM0NQpmdHBfdGNwOjM0NTY3CnJkcF90Y3A6MjM0MjE=`,如果觉得这样不安全的话,也可以修改脚本,使用一些别的办法进行加密和解密。 + +在实际使用的时候,可以使用脚本查询对应服务的端口号。 + +这样一来,只需要一个直连域名的A记录来记录公网IP,和一个TXT记录来记录服务暴露的公网端口, + + +## 使用方法(以SSH为例) +### 打洞时配置通知脚本参数 +```bash +python3 natter.py -e cf-redir.py -key ssh cf-service.py +``` + + +| -key 参数序号 | 参数说明 | 参数格式 | +|-----------|----------|------------------------| +| 1 | 服务名称 | `ssh`、`rdp` 或者你自己定义的名称 | +| 2 | 端口通知脚本路径 | 脚本路径 | + +### SSH客户端配置 +SSH客户端访问的时候,需要使用ProxyCommand来运行端口解析的脚本 + +假设你在 [cf-aio.conf](../scripts/cf-aio.conf) 中定义直连的域名为 **direct.example.com** ,TXT记录的POT域名为 **pot.example.com**,POT 服务的名称为 **ssh** + +由于采用TCP打洞的方式,因此实际使用的时候,key要改写为`ssh_tcp` + + +#### macOS/Linux 用户 +配置SSH config如下 +```bash +Host direct.example.com + ProxyCommand ~/.ssh/potssh.sh %h +``` +编辑 potssh.sh 中的 +```bash +#!/bin/sh +key=ssh_tcp # 你定义的POT KEY与协议类型的组合 +service_host=pot.example.com # host name for query services port + +host=$1 +raw=$(dig +short -t txt $service_host) +raw_formatted=$(echo $raw | sed 's/"//g') +str=$(echo $raw_formatted | base64 -d) +port=22 #default ssh port value + +if grep -qiE '^$key:' <<< "$str"; then + port=$(echo "$str" | grep '^ssh_tcp:' | cut -d ':' -f2) +fi + +echo "destination port: $port" +exec nc ${host} ${port} +``` + +`potssh.sh` 文件需要使用`chmod a+x` 赋予权限 + +#### Windows用户 +1. 下载并安装nmap: https://nmap.org/download.html#windows + - 安装时要勾选 Ncat (默认是勾选的) +2. 更新powershell执行策略 +以管理员权限启动 powershell 执行以下命令 +```shell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned +``` +3. 编辑 [potssh.ps1](../scripts/pot/potssh.ps1) 脚本,配置你的key和pot域名 +```bash +param( + [string]$sshhost, + [string]$key="ssh_tcp", # 你定义的POT KEY与协议类型的组合 + [string]$service_host="pot.example.com" +) + +# 使用 Resolve-DnsName 获取 TXT 记录 +$raw = (Resolve-DnsName -Name $service_host -Type TXT).Strings +$rawFormatted = $raw -replace '"', '' +$str = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($rawFormatted)) +Write-Host "destination rawf: $rawFormatted" +Write-Host "destination str: $str" + +$port = 22 + + +if ($str -match '^' + $([regex]::escape($key)) + ':\s*(\d+)$') { + $port = $Matches[1] + Write-Host "SSH port: $port" +} + +Write-Host "destination: $sshhost, port: $port" + +# 使用 ncat 进行连接 +ncat $sshhost $port +``` + +4. 编辑ssh配置,如果你的ps1脚本不在 `.ssh` 目录,要注意修改你的ps1脚本路径 +```bash +Host direct.example.com + ProxyCommand powershell ~/.ssh/potssh.ps1 %h +``` + +### SSH客户端访问 +连接ssh时,使用如下命令连接即可 +```bash +ssh username@direct.example.com +``` + +--- + +对于其他类型的服务,也可以采用类似的方法,通过对TXT记录进行解析后得到服务的端口号后再进行访问。 \ No newline at end of file diff --git a/docs/script.md b/docs/script.md index d5f174b..137d897 100644 --- a/docs/script.md +++ b/docs/script.md @@ -78,3 +78,4 @@ Natter 仓库中包含一些已经写好的通知脚本。您只需修改脚本 - [`tr.sh`](../natter-docker/transmission/tr.sh):Shell 脚本,用于更新 Transmission 监听端口,使其向 tracker 通告的端口号与外部端口一致; - [`cf-srv.py`](../natter-docker/minecraft/cf-srv.py):Python 脚本,用于更新 Cloudflare 域名的 A 记录和 SRV 记录,使得 Minecraft 等服务可通过域名直接访问。 - [`cf-redir.py`](../natter-docker/nginx-cloudflare/cf-redir.py):Python 脚本,用于实现 Cloudflare 的跳转功能,使得直接访问域名即可动态跳转到目标端口。 +- [`cf-aio.py`](../scripts/cf-aio.py):Python 脚本,整合了用于实现在 Cloudflare 记录信息的脚本。 详细使用参考 [CloudFlare All In One 脚本使用说明](cf-aio.md) 。 diff --git a/scripts/cf-aio.conf b/scripts/cf-aio.conf new file mode 100644 index 0000000..89f778a --- /dev/null +++ b/scripts/cf-aio.conf @@ -0,0 +1,11 @@ +{ + "cf_email":"email@example.com", + "cf_key":"d41d8cd98f00b204e9800998ecf8427e", + "direct_host":"direct.example.com", + "redirect_to_https":false, + "redirect_host":null, + "srv_host":null, + "srv_name":"_minecraft", + "pot_service_host":null, + "pot_service_key":"ssh" +} \ No newline at end of file diff --git a/scripts/cf-aio.py b/scripts/cf-aio.py new file mode 100755 index 0000000..974c04e --- /dev/null +++ b/scripts/cf-aio.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +import urllib.request +import json +import sys +import base64 +import os + +# Natter notification script arguments +protocol, private_ip, private_port, public_ip, public_port = sys.argv[1:6] + +cf_auth_email = None +cf_auth_key = None +cf_redirect_to_https = False +cf_direct_host = None +cf_redirect_host = None +cf_srv_host = None +cf_srv_name = None +cf_pot_service_host = None +cf_pot_service_key = None + +# 获取当前文件的绝对路径 +current_file_path = os.path.abspath(__file__) +# 获取当前文件所在的目录 +current_dir = os.path.dirname(current_file_path) +# 配置文件路径 +config_path = os.path.join(current_dir, "cf-aio.conf") + +def main(): + queryConfiguration() + print("email: %s, key: %s, redirect_to_https: %s, direct_host: %s, \ + redirect_host: %s, srv_host: %s, srv_name: %s ,pot_host: %s, pot_key: %s" % \ + (cf_auth_email, cf_auth_key, cf_redirect_to_https, cf_direct_host, \ + cf_redirect_host, cf_srv_host, cf_srv_name, \ + cf_pot_service_host, cf_pot_service_key)) + + cf = CloudFlareApi(cf_auth_email, cf_auth_key) + + if cf_direct_host is not None: + print(f"Setting [ {cf_direct_host} ] DNS to [ {public_ip} ] directly...") + cf.set_a_record(cf_direct_host, public_ip, False) + + if cf_redirect_host is not None: + print(f"Setting [ {cf_redirect_host} ] DNS to [ {public_ip} ] proxied by CloudFlare...") + cf.set_a_record(cf_redirect_host, public_ip, True) + if cf_direct_host is not None: + print(f"Setting [ {cf_redirect_host} ] redirecting to [ {cf_direct_host}:{public_port} ], https={cf_redirect_to_https}...") + cf.set_redirect_rule(cf_redirect_host, cf_direct_host, public_port, cf_redirect_to_https) + + if cf_srv_host is not None and cf_srv_name is not None: + print(f"Setting {cf_srv_host} A record to {public_ip}...") + cf.set_a_record(cf_srv_host, public_ip) + + print(f"Setting {cf_srv_host} SRV record to {protocol} port {public_port}...") + cf.set_srv_record(cf_srv_host, public_port, service=cf_srv_name, protocol=f"_{protocol}") + + if cf_pot_service_key is not None and cf_pot_service_host is not None: + service_key_combined = cf_pot_service_key + "_" + protocol + print("service_key_combined: " + service_key_combined) + print(f"Setting service [ {service_key_combined} ] port to [ {public_port} ] into [ {cf_pot_service_host} ] TXT record on CloudFlare...") + cf.set_txt_record(cf_pot_service_host, service_key_combined, public_port) + +def queryConfiguration(): + config = None + print("config path: %s" % config_path) + try: + with open(config_path, "r") as f: + config = json.load(f) + except FileNotFoundError as e: + print(e) + if config is None: + print("config not found, exit") + exit(1) + + global cf_auth_email + global cf_auth_key + global cf_redirect_to_https + global cf_direct_host + global cf_redirect_host + global cf_srv_host + global cf_srv_name + global cf_pot_service_host + global cf_pot_service_key + + cf_auth_email = config.get("cf_email", None) + cf_auth_key = config.get("cf_key", None) + cf_redirect_to_https = config.get("redirect_to_https", False) + cf_direct_host = config.get("direct_host", None) + cf_redirect_host = config.get("redirect_host", None) + cf_srv_host = config.get("srv_host", None) + cf_srv_name = config.get("srv_name", None) + cf_pot_service_host = config.get("pot_service_host", None) + cf_pot_service_key = config.get("pot_service_key", None) + +class CloudFlareApi: + 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, proxied=False): + 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, proxied) + else: + rec_id = self._update_a_record(zone_id, rec_id, name, ipaddr, proxied) + return rec_id + + def set_srv_record(self, name, port, service="_natter", protocol="_tcp"): + zone_id = self._find_zone_id(name) + if not zone_id: + raise ValueError("%s is not on CloudFlare" % name) + rec_id = self._find_srv_record(zone_id, name) + if not rec_id: + rec_id = self._create_srv_record(zone_id, name, service, + protocol, port, name) + else: + rec_id = self._update_srv_record(zone_id, rec_id, name, service, + protocol, port, name) + return rec_id + + def set_txt_record(self, name, key, port): + zone_id = self._find_zone_id(name) + if not zone_id: + raise ValueError("%s is not on CloudFlare" % name) + rec_id = self._find_txt_record(zone_id, name) + if not rec_id: + rec_id = self._create_txt_record(zone_id, name, key, port) + else: + print("rec_id is: " + rec_id) + rec_id = self._update_txt_record(zone_id, rec_id, name, key, port) + return rec_id + + def set_redirect_rule(self, redirect_host, direct_host, public_port, https): + zone_id = self._find_zone_id(redirect_host) + ruleset_id = self._get_redir_ruleset(zone_id) + if not ruleset_id: + ruleset_id = self._create_redir_ruleset(zone_id) + rule_id = self._find_redir_rule(zone_id, ruleset_id, redirect_host) + if not rule_id: + rule_id = self._create_redir_rule(zone_id, ruleset_id, redirect_host, direct_host, public_port, https) + else: + rule_id = self._update_redir_rule(zone_id, ruleset_id, rule_id, redirect_host, direct_host, public_port, https) + 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: + 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" + ) + 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=False, 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=False, 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 _find_srv_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"] == "SRV" and rec_data["data"]["name"] == name: + rec_id = rec_data["id"] + return rec_id + return None + + def _create_srv_record(self, zone_id, name, service, protocol, port, target, + priority=1, weight=10, ttl=120): + name = name.lower() + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", + data={ + "data": { + "name": name, + "port": port, + "priority": priority, + "proto": protocol, + "service": service, + "target": target, + "weight": weight + }, + "proxied": False, + "type": "SRV", + "ttl": ttl + }, + method="POST" + ) + return data["result"]["id"] + + def _update_srv_record(self, zone_id, rec_id, name, service, protocol, port, target, + priority=1, weight=10, ttl=120): + name = name.lower() + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}", + data={ + "data": { + "name": name, + "port": port, + "priority": priority, + "proto": protocol, + "service": service, + "target": target, + "weight": weight + }, + "proxied": False, + "type": "SRV", + "ttl": ttl + }, + method="PUT" + ) + return data["result"]["id"] + + def _find_txt_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"] == "TXT" and rec_data["name"] == name: + rec_id = rec_data["id"] + return rec_id + return None + + def _create_txt_record(self, zone_id, name, key, port, ttl=120): + name = name.lower() + content_combined = key+":"+port + encoded_content = base64.b64encode(content_combined.encode()).decode() + print("encoded content: " + encoded_content) + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", + data={ + "content": encoded_content, + "name": name, + "type": "TXT", + "ttl": ttl + }, + method="POST" + ) + return data["result"]["id"] + + def _update_txt_record(self, zone_id, rec_id, name, key, port, ttl=120): + name = name.lower() + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}", + data={ + "name": name, + "type": "TXT", + }, + method="GET" + ) + b64content = data["result"]["content"] + decoded_message = "" + try: + decoded_message = base64.b64decode(b64content).decode() + except Exception as e: + print(f"Error decoding base64 content: {e}") + + print("decode string: " + decoded_message) + + key_found = False + need_update = False + + updated_text = [] + for line in decoded_message.splitlines(): + service, sport = line.split(":") + if service == key: + print("found service: " + service) + key_found = True + if sport == port: + print("port not changed") + else: + print("port changed to " + port) + sport = port + need_update = True + updated_text.append(f"{service}:{sport}") + + + if not need_update and key_found: + print("no need to update, exit") + return data["result"]["id"] + + if not key_found: + updated_text.append(f"{key}:{port}") + + new_txt_record = "\n".join(updated_text) + print("updated message: " + new_txt_record) + + new_txt_base64 = base64.b64encode(new_txt_record.encode()).decode() + + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}", + data={ + "content": new_txt_base64, + "name": name, + "type": "TXT", + "ttl": ttl + }, + method="PUT" + ) + + return data["result"]["id"] + + def _get_redir_ruleset(self, zone_id): + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets" + ) + for ruleset_data in data["result"]: + if ruleset_data["phase"] == "http_request_dynamic_redirect": + ruleset_id = ruleset_data["id"] + return ruleset_id + return None + + def _create_redir_ruleset(self, zone_id): + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets", + data={ + "name": "Redirect rules ruleset", + "kind": "zone", + "phase": "http_request_dynamic_redirect", + "rules": [] + }, + method="POST" + ) + return data["result"]["id"] + + def _get_description(self, redirect_host): + return f"Natter: {redirect_host}" + + def _find_redir_rule(self, zone_id, ruleset_id, redirect_host): + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/{ruleset_id}" + ) + if "rules" not in data["result"]: + return None + for rule_data in data["result"]["rules"]: + if rule_data["description"] == self._get_description(redirect_host): + rule_id = rule_data["id"] + return rule_id + return None + + def _create_redir_rule(self, zone_id, ruleset_id, redirect_host, direct_host, public_port, https): + proto = "http" + if https: + proto = "https" + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/{ruleset_id}/rules", + data={ + "action": "redirect", + "action_parameters": { + "from_value": { + "status_code": 302, + "target_url": { + "expression": f'concat("{proto}://{direct_host}:{public_port}", http.request.uri.path)' + }, + "preserve_query_string": True + } + }, + "description": self._get_description(redirect_host), + "enabled": True, + "expression": f'(http.host eq "{redirect_host}")' + }, + method="POST" + ) + for rule_data in data["result"]["rules"]: + if rule_data["description"] == self._get_description(redirect_host): + rule_id = rule_data["id"] + return rule_id + raise RuntimeError("Failed to create redirect rule") + + def _update_redir_rule(self, zone_id, ruleset_id, rule_id, redirect_host, direct_host, public_port, https): + proto = "http" + if https: + proto = "https" + data = self._url_req( + f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id}", + data={ + "action": "redirect", + "action_parameters": { + "from_value": { + "status_code": 302, + "target_url": { + "expression": f'concat("{proto}://{direct_host}:{public_port}", http.request.uri.path)' + }, + "preserve_query_string": True + } + }, + "description": self._get_description(redirect_host), + "enabled": True, + "expression": f'(http.host eq "{redirect_host}")' + }, + method="PATCH" + ) + for rule_data in data["result"]["rules"]: + if rule_data["description"] == self._get_description(redirect_host): + rule_id = rule_data["id"] + return rule_id + raise RuntimeError("Failed to update redirect rule") + +if __name__ == "__main__": + main() diff --git a/scripts/pot/potssh.ps1 b/scripts/pot/potssh.ps1 new file mode 100644 index 0000000..232882a --- /dev/null +++ b/scripts/pot/potssh.ps1 @@ -0,0 +1,25 @@ +param( + [string]$sshhost, + [string]$key="ssh_tcp", + [string]$service_host="pot.example.com" +) + +# 使用 Resolve-DnsName 获取 TXT 记录 +$raw = (Resolve-DnsName -Name $service_host -Type TXT).Strings +$rawFormatted = $raw -replace '"', '' +$str = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($rawFormatted)) +Write-Host "destination rawf: $rawFormatted" +Write-Host "destination str: $str" + +$port = 22 + + +if ($str -match '^' + $([regex]::escape($key)) + ':\s*(\d+)$') { + $port = $Matches[1] + Write-Host "SSH port: $port" +} + +Write-Host "destination: $sshhost, port: $port" + +# 使用 ncat 进行连接 +ncat $sshhost $port \ No newline at end of file diff --git a/scripts/pot/potssh.sh b/scripts/pot/potssh.sh new file mode 100644 index 0000000..ac4c10a --- /dev/null +++ b/scripts/pot/potssh.sh @@ -0,0 +1,16 @@ +#!/bin/sh +key=ssh_tcp # your service key +service_host=pot.example.com # host name for query services port + +host=$1 +raw=$(dig +short -t txt $service_host) +raw_formatted=$(echo $raw | sed 's/"//g') +str=$(echo $raw_formatted | base64 -d) +port=22 #default ssh port value + +if grep -qiE "^$key:" <<< "$str"; then + port=$(echo "$str" | grep '^ssh_tcp:' | cut -d ':' -f2) +fi + +echo "destination port: $port" +exec nc ${host} ${port} \ No newline at end of file