heiher / natmap

TCP/UDP port mapping for full cone NAT
MIT License
1.34k stars 101 forks source link

[shares] usage and demos #13

Open heiher opened 1 year ago

heiher commented 1 year ago

Welcome to share the usage and demos. 欢迎分享用法、示例。

heiher commented 1 year ago

TCP

UDP

Mythologyli commented 1 year ago

今天刚写的 qBittorrent 打洞脚本,可能不太完善,欢迎大家反馈: https://github.com/Mythologyli/qBittorrent-NAT-TCP-Hole-Punching

OpportunityLiu commented 1 year ago

尝试实现了 NAT-PMP 协议给 Transmission 用 ~,但是它的支持好像有问题,给他返回映射端口后,它把 IPv6 的监听端口也改了,导致 IPv6 入站全被防火墙拦截了。。。~ 应该没问题,就是一时没有v6入站

https://github.com/OpportunityLiu/nat-mapmp

image

image

EkkoG commented 1 year ago

使用NATMap在NAT-1私网IP宽带上部署 Trojan 服务,并通过其访问内网服务(回家)

wits-fe commented 1 year ago

uTorrent / qBittorrent / Transmisson 自动更新端口脚本

heyeshuang commented 1 year ago

对于wireguard方式,写了一个PowerShell脚本,能够自动修改配置文件的Endpoint并调用wireguard.exe进行连接。

使用方法:

  1. 安装wireguard-windows,用客户端连接测试成功。
  2. 在文件夹C:\example下建立wg.ps1nat.conf,粘贴Gist内容。
  3. 按照实际情况修改nat.conf,以及wg.ps1$Hostname部分。Endpoint不必修改。
  4. 以管理员身份运行PowerShell
  5. 设置ps1脚本运行权限:Set-ExecutionPolicy RemoteSigned(或Unrestricted)
  6. 启动Wireguard:C:\example\wg.ps1 -up
  7. 停止Wireguard:C:\example\wg.ps1 -down

在Windows 11, Powershell 5.1.22621.963测试通过,也可以配合Windows下的sudo使用。

另外,在Android下,也可以用termux运行nm-echo.sh来获得IP地址,可以不必更换客户端。

wy580477 commented 1 year ago

TCP 打洞部署 Vmess TCP 代理服务 让流量回家,通过脚本生成 vmess 分享链接保持更新。

对比 wireguard 优点:

  1. 几乎所有的翻墙代理客户端都可以支持,无需安装专用改版客户端。
  2. Vmess 服务端部署更简单更轻量,只需要运行代理服务端程序即可,无需任何系统特权修改系统设置。
  3. TCP 协议传输,可以解决某些网络环境下 UDP 被限制的问题。

SIng-box 服务端 Vmess 配置示例:

{
  "log": {
    "level": "info"
  },
  "inbounds": [
    {
      "type": "vmess",
      "listen": "0.0.0.0",
      "listen_port": 9689,
      "users": [
        {
          "uuid": "20a46c57-710e-4ec9-947d-2c178f037bf5",
          "alterId": 0
        }
      ],
      "sniff": true,
      "sniff_override_destination": false
    }
  ]
}

配合 natmap 的 linux 脚本(需要 base64 命令,openwrt 通过 opkg install coreutils-base64 安装):

#!/bin/sh

# 服务器别名
server_alias=Home_Proxy

# 服务器地址
server_address="$1"

# 服务器端口
server_port="$2"

# 用户 UUID
user_id="20a46c57-710e-4ec9-947d-2c178f037bf5"

# 生成的 Vmess 分享链接文件位置
share_link_file="/www/ad874236-07ed-4801-99f0"

# 生成 VMess 链接
vmess_link="vmess://$(echo -n "{\"v\":\"2\",\"ps\":\"$server_alias\",\"add\":\"$server_address\",\"port\":$server_port,\"id\":\"$user_id\",\"aid\":\"0\",\"net\":\"tcp\",\"type\":\"none\"}" | base64 -w 0)"

echo $vmess_link > $share_link_file

生成的 分享链接 文件可以通过 内网穿透 或者 增加 curl 命令上传至 web 服务暴露出来。 然后代理客户端,将 url 地址填入订阅功能即可。

注意:

  1. 生成的订阅文件一定要使用复杂不规律文件名/路径,而且订阅 url 一定要用 https 加密保证安全。
  2. 代理客户端可能默认局域网地址段直连,这时需要在代理客户端路由功能中把家里局域网 ip 段设置为走代理。
  3. 多人使用的情况下,可以通过代理服务端的路由功能,限制对内网地址段的访问。
OpenGG commented 1 year ago

Resolve IP4P and generate config with cloudflare worker.

worker.js:

/**
 * Purpose: Resolve IP4P and generate a configuration using a Cloudflare worker.
 * 
 * Usage:
 * 1. Create an online configuration file with placeholders: ${ip4p.ip}, ${ip4p.port}, ${query.xxx}.
 * 2. Set your online configuration URL as the CONFIG_URL variable and publish this script as a Cloudflare worker.
 * 3. Retrieve your generated configuration by accessing https://YOUR-WORKER.workers.dev/YOUR-random-PATH-12435/clash?IP4P_DOMAIN=YOUR_IP4P_DOMAIN&cipher=YOUR_CIPHER&password=YOUR_PASSWORD.
 * 4. Additional keyword checks to the user-agent header can be applied, by setting ALLOW_UA_KEYWORDS.
 * 
 * Notes:
 * 1. Only IP4P_DOMAIN is mandatory; the rest of the query parameters are optional.
 * 2. The configuration itself can be in any format you like (yaml, json, etc.).
 * 3. Add ?_= to the CONFIG_URL to prevent caching.
 * 4. Choose a random PATH to prevent URL leakage.
 * 5. If the worker's domain is blocked in your region, consider binding the worker to your custom domain.
 */

// Set your online configuration URL here
const CONFIG_URL = 'https://gist.githubusercontent.com/YOUR_ONLINE_CONFIG/config-ss.yaml?_=';

// Choose a random path to prevent URL leakage
const PATH = '/YOUR-random-PATH-12435/clash';

// Keywords to allow in user-agent header
// const ALLOW_UA_KEYWORDS = 'clash,Clash,v2ray'
const ALLOW_UA_KEYWORDS = ''

// Cloudflare DNS-over-HTTPS URL
const DOH_URL = 'https://cloudflare-dns.com/dns-query?ct=application/dns-json';

const ALLOW_UA_KEYWORDS_ARR = ALLOW_UA_KEYWORDS.split(',')
  .filter(keyword => keyword)

/**
 * Performs an HTTP GET request.
 * 
 * @param {string | URL} url - The URL to fetch.
 * @returns {Promise<Response>} A promise that resolves to the fetch response.
 */
const get = async (url) => {
  const res = await fetch(url)

  if (!res.ok) {
    throw new Error(`Request error: ${res.status}`)
  }

  return res
}

/**
 * Resolves DNS records using DNS-over-HTTPS.
 * 
 * @param {string} domain - The domain to resolve.
 * @param {string} [type='AAAA'] - The DNS record type (default: 'AAAA').
 * @returns {Promise<Object>} A promise that resolves to the DNS response JSON object.
 */
const resolveDNSRecord = async (domain, type = 'AAAA') => {
  const url = new URL(DOH_URL)

  url.searchParams.append('name', domain)
  url.searchParams.append('type', type)

  const res = await get(url)

  return res.json()
}

/**
 * @typedef {Object} IP4PInfo
 * @property {string} ip - The IP address.
 * @property {number} port - The port number.
 */

/**
 * Resolves IP4P information from DNS records.
 *
 * @param {string} domain - The domain to resolve IP4P for.
 * @returns {Promise<IP4PInfo>} A promise that resolves to an object containing IP and port.
 * @throws {Error} If the IP4P information is invalid.
 */
const resolveIP4P = async (domain) => {
  const json = await resolveDNSRecord(domain)

  const answer = json?.Answer

  if (!answer || !Array.isArray(answer)) {
    throw new Error('Invalid dns record')
  }

  const data = answer.find(t => t.data)?.data || ''

  const parts = data.split(':')
  if (parts.length !== 5) {
    throw new Error(`Invalid IP4P: ${data}`)
  }

  // See: https://github.com/heiher/natmap/wiki/ssh#proxycommand

  const port = parseInt(parts[2], 16)
  const ipab = parseInt(parts[3], 16)
  const ipcd = parseInt(parts[4], 16)

  if (Number.isNaN(port) || Number.isNaN(ipab) || Number.isNaN(ipcd)) {
    throw new Error(`Invalid IP4P values: ${data}`)
  }

  const ipa = ipab >> 8
  const ipb = ipab & 0xff
  const ipc = ipcd >> 8
  const ipd = ipcd & 0xff

  const ip = `${ipa}.${ipb}.${ipc}.${ipd}`

  return {
    ip,
    port,
  }
}

/**
 * Gets the configuration from the online source with placeholders replaced.
 *
 * @param {string} url - The URL of the configuration source.
 * @param {(type: string, key: string) => string | undefined} replacer - A function that replaces placeholders based on their type and key.
 * @returns {Promise<string>} A promise that resolves to the configuration with placeholders replaced.
 */
const getConfig = async (url, replacer) => {
  const urlObject = new URL(url)
  const {
    searchParams,
  } = urlObject

  if (searchParams.has('_')) {
    searchParams.set('_', `${Math.random()}`)
  }

  const res = await get(urlObject)

  const configText = await res.text()

  return configText.replace(/\$\{([^}]+)\}/g, (g0, g1) => {
    const index = g1.indexOf('.')
    if (index === -1) {
      return g0
    }

    const type = g1.slice(0, index)

    const key = g1.slice(index + 1)

    const value = replacer(type, key)

    if (typeof value === 'string') {
      return value
    }

    return g0
  })
}

/**
 * Checks if the user-agent header is allowed.
 *
 * @param {string} ua - The user-agent header.
 * @returns {boolean} True if user-agent is allowed, false otherwise.
 */
const allowUA = (ua) => {
  if (ALLOW_UA_KEYWORDS_ARR.length === 0) {
    return true
  }

  return ALLOW_UA_KEYWORDS_ARR.some(keyword => ua.includes(keyword))
}

/**
 * Main function to handle requests.
 *
 * @param {Request} request - The request object.
 * @returns {Promise<string>} A promise that resolves to the response.
 * @throws {Error}
 */
const main = async (request) => {
  const {
    url,
    headers,
  } = request

  const ua = headers.get('user-agent')

  if (!allowUA(ua)) {
    throw new Error('Invalid user-agent, failed to pass keyword checking')
  }

  const {
    pathname,
    searchParams,
  } = new URL(url)

  if (pathname !== PATH) {
    throw new Error(`Unknown request: ${pathname}`)
  }

  const domain = searchParams.get('IP4P_DOMAIN')

  if (!domain) {
    throw new Error('Domain name not provided')
  }

  const ip4p = await resolveIP4P(domain)

  const config = await getConfig(CONFIG_URL, (type, key) => {
    if (type === 'ip4p' && (key === 'ip' || key === 'port')) {
      return `${ip4p[key]}`
    } else if (type === 'query') {
      return searchParams.get(key) || ''
    }

    return undefined;
  })

  return config
}

// Cloudflare worker export
export default {
  /**
   * Cloudflare Worker Fetch Function.
   *
   * @param {Request} request - The incoming request object.
   * @param {Object} env - The environment object.
   * @param {Object} ctx - The context object.
   * @returns {Promise<Response>} A promise that resolves to the response.
   */
  async fetch(request, env, ctx) {
    try {
      const content = await main(request)

      return new Response(content, {
        status: 200,
        headers: {
          'cache-control': 'no-cache, no-store'
        }
      })
    } catch (e) {
      console.error(e)

      return new Response('', {
        status: 404,
        headers: {
          'cache-control': 'no-cache, no-store'
        }
      })
    }
  },
}

Config file (can be yaml, json, whatever format you like):

mixed-port: 7890

mode: rule

ipv6: true
dns:
  ipv6: true

proxies:
  -
    name: proxy-server
    type: ss
    server: ${ip4p.ip}
    port: ${ip4p.port}
    cipher: "${query.cipher}"
    password: "${query.password}"
    udp: true

proxy-groups:
  -
    name: PROXY
    type: select
    proxies:
      - proxy-server
      - DIRECT

rule-providers:
  reject:
    type: http
    behavior: domain
    url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt"
    path: ./ruleset/reject.yaml
    interval: 86400

  private:
    type: http
    behavior: domain
    url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/private.txt"
    path: ./ruleset/private.yaml
    interval: 86400

  gfw:
    type: http
    behavior: domain
    url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt"
    path: ./ruleset/gfw.yaml
    interval: 86400

  tld-not-cn:
    type: http
    behavior: domain
    url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/tld-not-cn.txt"
    path: ./ruleset/tld-not-cn.yaml
    interval: 86400

  telegramcidr:
    type: http
    behavior: ipcidr
    url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/telegramcidr.txt"
    path: ./ruleset/telegramcidr.yaml
    interval: 86400

  applications:
    type: http
    behavior: classical
    url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/applications.txt"
    path: ./ruleset/applications.yaml
    interval: 86400

rules:
  - RULE-SET,applications,DIRECT
  - RULE-SET,private,DIRECT
  - RULE-SET,reject,REJECT
  - RULE-SET,tld-not-cn,PROXY
  - RULE-SET,gfw,PROXY
  - RULE-SET,telegramcidr,PROXY
  - MATCH,DIRECT
xream commented 8 months ago

image

https://github.com/sub-store-org/Sub-Store 的节点域名解析支持了 IP4P

xream commented 7 months ago

clash.meta(mihomo) 已支持 IP4P 出站

感谢 亚托莉佬以及 mihomo 开发组接受我的建议(

用法: 开启 IPv6, 在需要使用的地方配置 IP4P 域名.

IP4P 节点和 ip4p.web.com 这个服务可以正常使用

ipv6: true
dns:
  ipv6: true
experimental:
  dialer-ip4p-convert: true
proxies:
  - name: IP4P
    server: ip4p.proxy.com
    port: 1
    ...
rules:
  - DOMAIN,ip4p.web.com,DIRECT
xream commented 7 months ago

IP4P 请求自动重定向(以 Surge 为例)

效果为 访问 http://ip4p.com/a?v=1 时, 自动根据 IP4P 信息重定向为 http://1.1.1.1:1234/a?v=1

使用场景

使用固定的 URL 访问 STUN 打洞的内网服务

模块和脚本见 https://t.me/zhetengsha/1198

heyeshuang commented 6 months ago

TCP 打洞部署 Vmess TCP 代理服务 让流量回家,通过脚本生成 vmess 分享链接保持更新。

可以利用自部署的pastebin服务,例如SharzyL/pastebin-worker,来提供分享链接,不必暴露自己的web服务。

配合 natmap 的 linux 脚本(也需要 base64 命令):

#!/bin/bash

ip_address="${1}"
port="${2}"
pastebin_url="https://shz.al/"
pastebin_name="<随机字符串a>"
pb_pass="<随机字符串b>"
raw_ss_url="ss://2022-blake3-aes-128-gcm:<密码>@${ip_address}:${port}#ss-home4
ss://2022-blake3-aes-128-gcm:<密码>@<其他地址>#ss-home6
"

# Apply base64 encoding
base64_encoded=$(echo -n "${raw_ss_url}" | base64 -w 0)

# Upload the result using curl
curl -Fc="${base64_encoded}" -Fe="24M" -Fs="${pb_pass}" -Fn="${pastebin_name}" "${pastebin_url}"
curl -X PUT -Fc="${base64_encoded}" -Fe="24M" "${pastebin_url}~${pastebin_name}:${pb_pass}"

然后就可以用https://shz.al/~<随机字符串a>作为订阅地址了。

yv-code commented 6 months ago

我尝试着给 WireGuard iOS 官方客户端添加了 IP4P 支持,自己用下来没有问题,这里分享出来。

用法也很简单,和 Android 基本一样,不过会判断 peer 端口填写为 0 才会开启 IP4P 解析。

https://github.com/yv-code/wireguard-apple

感谢各位开源作者。

rampageX commented 5 months ago

截至 2024.06.03 , 使用NATMap在NAT-1私网IP宽带上部署Web服务 一文中的 wdns 脚本已不能正常运作,应该是 Cloudflare 改了 API,故更新脚本。脚本中的子域名,Origin 规则(名称默认 natmap) 都需要在面板中预先创建。脚本需要 jq 支持。

#!/bin/sh

TOKEN='EvkRQ-23424234234_j0X7x_Z1tVs3ngc9VUoC'
ZONE="test.tt.com"
SUB_DOMAIN="home"
ORIGIN_RULE="natmap"
DOMAIN="${SUB_DOMAIN}.${ZONE}"

ZONES_FILE="/tmp/zones.json"
DNS_RECORDS_FILE="/tmp/dns_records.json"

ADDR=${1}
PORT=${2}

#GET ZONE_ID:
[ ! -e $ZONES_FILE ] && {
curl -sX GET "https://api.cloudflare.com/client/v4/zones" \
    -H "Authorization: Bearer ${TOKEN}" \
    -H "Content-Type:application/json" | jq .result > $ZONES_FILE
}

ZONE_NUM=$(jq '. | length' $ZONES_FILE)
ZONE_NUM_FOR=$(expr $ZONE_NUM - 1)

for i in $(seq 0 $ZONE_NUM_FOR); do
    zone="$(jq -r ".[$i].name" $ZONES_FILE | sed 's/"//')"
    [ $zone = "$ZONE" ] && {
        ZONE_ID="$(jq -r ".[$i].id" $ZONES_FILE | sed 's/"//')"
        break
    }
done

#GET RECORD_ID:
[ ! -e $DNS_RECORDS_FILE ] && {
    curl -sX GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
        -H "Authorization: Bearer ${TOKEN}" \
        -H "Content-Type:application/json" | jq .result > $DNS_RECORDS_FILE
}

DNS_RECORDS_NUM=$(jq '. | length' $DNS_RECORDS_FILE)
DNS_RECORDS_NUM_FOR=$(expr $DNS_RECORDS_NUM - 1)

for i in $(seq 0 $DNS_RECORDS_NUM_FOR); do
    name="$(jq -r ".[$i].name" $DNS_RECORDS_FILE | sed 's/"//')"
    [ $name = "$DOMAIN" ] && {
        RECORD_ID="$(jq -r ".[$i].id" $DNS_RECORDS_FILE | sed 's/"//')"
        break
    }
done

# DNS
while true; do
    curl -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
        -H "Authorization: Bearer ${TOKEN}" \
        -H "Content-Type:application/json" \
        --data "{\"type\":\"A\",\"name\":\"${SUB_DOMAIN}\",\"content\":\"${ADDR}\",\"ttl\":60,\"proxied\":false}"  > /dev/null 2> /dev/null
    if [ $? -eq 0 ]; then
        break
    fi
done

# Origin rule
while true; do
    curl -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_request_origin/entrypoint" \
         -H "Authorization: Bearer ${TOKEN}" \
         -H "Content-Type: application/json" \
         --data "{\"rules\":[{\"expression\":\"(http.host eq \\\"${DOMAIN}\\\")\",\"description\":\"${ORIGIN_RULE}\",\"action\":\"route\",\"action_parameters\":{\"origin\":{\"port\":${PORT}}}}]}"  > /dev/null 2> /dev/null
    if [ $? -eq 0 ]; then
        break
    fi
done
lalasou commented 4 months ago

cf 重定向 脚本 设置好cf 重定向 然后 api 地址浏览器F12抓包下


#!/bin/bash
IP="$1"
IPport="$2"
IPadd="http://${1}:${2}"

sleep 1

logger -t natmap "地址'$IPadd'"
curl -L -k -s "https://api.cloudflare.com/client/v4/zones/9f98e1aa7ed47633335d3570af/rulesets/defe98d8036c400b86111111111/rules/5ba6ee1b7fb4407699182222222220" \
     -X 'PATCH' \
     -H "Authorization: Bearer HWIucLnR7dfsJtqs-QGa7yTiKDSKHi7RN9N" \
     -H "Content-Type:application/json" \
     --data '{"description":"mt","expression":"(http.host eq \"m.xx.us.kg\")","action":"redirect","action_parameters":{"from_value":{"status_code":301,"preserve_query_string":true,"target_url":{"expression":"concat(\"http://'$IP':'$IPport'\", http.request.uri.path)"}}},"enabled":true}'
sleep 1
kwxiaozhu commented 1 month ago

wireguard windows便携版客户端,支持IP4P解析,端口随意写,域名解析到IP4P地址时会自动解析IP和端口。 https://github.com/kwxiaozhu/wireguard-windows-portable 缝合两位大佬的成果,实现便携客户端以及IP4P解析。 感谢各位开源作者。

seanwang881 commented 1 month ago

wireguard windows便携版客户端,支持IP4P解析,端口随意写,域名解析到IP4P地址时会自动解析IP和端口。 https://github.com/kwxiaozhu/wireguard-windows-portable 缝合两位大佬的成果,实现便携客户端以及IP4P解析。 感谢各位开源作者。

可否增加一个TXT解析功能呢?有些环境无法解析IPV6/IP4P

heiher commented 1 month ago

wireguard windows便携版客户端,支持IP4P解析,端口随意写,域名解析到IP4P地址时会自动解析IP和端口。 https://github.com/kwxiaozhu/wireguard-windows-portable 缝合两位大佬的成果,实现便携客户端以及IP4P解析。 感谢各位开源作者。

可否增加一个TXT解析功能呢?有些环境无法解析IPV6/IP4P

我之前的实现(使用系统DNS解析器)确实在某些系统上存在无法解析AAAA记录的情况,看这版实现可能是可以的,建议实测看看。

kwxiaozhu commented 1 month ago

这个默认使用223.5.5.5的DNS解析,没有使用系统DNS解析,理论上不存在无法解析AAAA记录的情况,我实测只要能联网的环境都可以使用(PS:如果局域网劫持所有53端口的DNS解析请求到不支持解析AAAA记录的内网DNS另说。。。)。 另外https://github.com/kwxiaozhu/wireguard-android-ip4p 这个项目使用的是DOH 来解析DNS,实测可以正常解析AAAA记录。

wireguard windows便携版客户端,支持IP4P解析,端口随意写,域名解析到IP4P地址时会自动解析IP和端口。 https://github.com/kwxiaozhu/wireguard-windows-portable 缝合两位大佬的成果,实现便携客户端以及IP4P解析。 感谢各位开源作者。

可否增加一个TXT解析功能呢?有些环境无法解析IPV6/IP4P

seanwang881 commented 1 month ago

这个默认使用223.5.5.5的DNS解析,没有使用系统DNS解析,理论上不存在无法解析AAAA记录的情况,我实测只要能联网的环境都可以使用(PS:如果局域网劫持所有53端口的DNS解析请求到不支持解析AAAA记录的内网DNS另说。。。)。 另外https://github.com/kwxiaozhu/wireguard-android-ip4p 这个项目使用的是DOH 来解析DNS,实测可以正常解析AAAA记录。

wireguard windows便携版客户端,支持IP4P解析,端口随意写,域名解析到IP4P地址时会自动解析IP和端口。 https://github.com/kwxiaozhu/wireguard-windows-portable 缝合两位大佬的成果,实现便携客户端以及IP4P解析。 感谢各位开源作者。

可否增加一个TXT解析功能呢?有些环境无法解析IPV6/IP4P

感谢,heiher 的版本一直会触发更新,显示"正在获取更新元数据",你的是否可以取消它的更新检测?

kwxiaozhu commented 1 month ago

这个默认使用223.5.5.5的DNS解析,没有使用系统DNS解析,理论上不存在无法解析AAAA记录的情况,我实测只要能联网的环境都可以使用(PS:如果局域网劫持所有53端口的DNS解析请求到不支持解析AAAA记录的内网DNS另说。。。)。 另外https://github.com/kwxiaozhu/wireguard-android-ip4p 这个项目使用的是DOH 来解析DNS,实测可以正常解析AAAA记录。

wireguard windows便携版客户端,支持IP4P解析,端口随意写,域名解析到IP4P地址时会自动解析IP和端口。 https://github.com/kwxiaozhu/wireguard-windows-portable 缝合两位大佬的成果,实现便携客户端以及IP4P解析。 感谢各位开源作者。

可否增加一个TXT解析功能呢?有些环境无法解析IPV6/IP4P

感谢,heiher 的版本一直会触发更新,显示"正在获取更新元数据",你的是否可以取消它的更新检测?

没做处理,目前是基于最新版改的,没弹更新,后面有更新了我再跟进,升级逻辑在这里 ui/src/main/java/com/wireguard/android/updater/Updater.kt,可以问问chatgpt自己改改