每日签到奶昔超市积分商城奶昔访达
12下一页
返回列表 发布新帖

我终于知道了为什么WiFi calling这么难拉起

 
3333 41
发表于 2026-7-30 22:31:29 | 查看全部 阅读模式

登录后免广告,享受更多奶昔会员权益!

您需要 登录 才可以下载或查看,没有账号?注册

×

之前试过WiFi calling没拉起来,现在要把giffgaff 转 CTExcel  没办法,又要拉WiFi calling.

经过我尝试了两天,我知道了WiFi Calling 为什么这么难拉。

先直接说结论,WiFi Calling拉不起来最大的障碍就是机场的节点不支持 500 和 4500 的 UDP端口。 并不是什么IP不纯净,跟IP没有关系,就是UDP这两端口不支持。
准确的说是连接 pub.3gppnetwork.org 测试发现  500 和 4500 的 UDP端口 不通。

很多机场节点订阅显示支持UDP,但实际上根本就不支持500和4500端口,甚至UDP本身只是开关上支持,一连就是失败。我特地让Codex帮我写了一个Python脚本测试,发现基本都不行,加上前两天买的红杏云也不行了,机场支持UDP 特别是 500 和 4500很少,我觉得比较可行的方式只能自己去搭建VPS吧。

下面几个分享踩过的坑吧:
1. 靠小火箭或其他APP确实拉不起来
使用小火箭配置Wifi开关开下关下的方式,只能说有概率能拉。尝试过很多次,拉起的概率不高,要试n次才能拉起成功一次。 而且就算拉起成功了, 发现 CTExcel 仍然呼叫不了888。
小火箭我之前也按油管教程设置的,有概率能拉,但是确实呼叫不了888, 原因就是小火箭的方式确实是有些问题  (因为我后面用软路由来拉的话,非常顺畅,几秒钟就能拉起。但小火箭拉的速度就很慢,而且拉的也不成功,概率也是只有30%)。

另外,最开始我还尝试过使用免费的 Clash Mi 来拉 Wifi Calling, 这个一次都没成功过。
所以这个问题结论要拉WiFi Calling 必须要使用软路由的方式,或者使用其他的电脑或安卓手机开启WiFi代理的方式。不能单纯使用小火箭拉。

2. 使用软路由需要配置过滤和分流
我换用自己的软路由来拉 WiFi Calling,我的配置用的是 OpenClash。配置了全局代理模式但之前我也试了很多次,拉不起来。
后面发现是这里又有一个坑点,就是clash使用Fake IP模式的时候,必须要Fake-IP-Filter 过滤

  1. +.pub.3gppnetwork.org
复制代码


另外可以设置分流规则, 这样就不需要设置全局代理了。

  1. rules:
  2. # 地区检测
  3. DOMAIN-SUFFIX, gspe1-ssl.ls.apple.com
  4. # 沃达丰
  5. DOMAIN-SUFFIX, epdg.epc.mnc015.mcc234.pub.3gppnetwork.org
  6. DOMAIN-SUFFIX, ss.epdg.epc.mnc015.mcc234.pub.3gppnetwork.org
  7. DOMAIN-SUFFIX, ss.epdg.epc.geo.mnc015.mcc234.pub.3gppnetwork.org
  8. DOMAIN-SUFFIX, entsrv-uk.vodafone.com
  9. DOMAIN-SUFFIX, vuk-gto.prod.ondemandconnectivity.com
  10. IP-CIDR,88.82.0.0/19
  11. # CMLinkUK EE
  12. IP-CIDR,46.68.0.0/17
  13. # Giffgaff
  14. IP-CIDR,87.194.0.0/16
复制代码

参考: OpenWRT软路由分流规则开启英国电话卡(Giffgaff)wificalling功能

3. 国产安卓想要拉起WiFi Calling 非常难,不建议尝试。
我测试过我的一加6T,还有OPPO手机,配置好了  Shizuku + Pixel IMS 仍然不行, 估计是 IMS 不行吧
有兴趣和时间折腾的可以谷歌搜索参考这两篇文章,我放不了链接~
在 Pixel 6 Pro 上开启 GiffGaff WiFi Calling
华为、小米和一加等国产安卓手机开通WIFI Calling(VoWIFI)

所以建议用 IOS 开启 Wifi Calling,26以上的设备用WOL 虚拟定位的方式, 油管搜索 iOS 虚拟定位 有很详细的教程

其他大概就这些了吧,也折腾了很多。
爱生活,爱奶昔~
estkme

评论41

tudouy楼主Lv.2 发表于 2026-7-31 15:47:17 | 查看全部
nqwe 发表于 2026-7-31 03:54
测试脚本可以分享一下吗,十分需要了

也可以,我附在这里,你看看。 或者你可以试试让Codex写一个
  1. #!/usr/bin/env python3
  2. """Probe SOCKS5 UDP and Vodafone WiFi Calling IKE reachability."""

  3. from __future__ import annotations

  4. import argparse
  5. import hashlib
  6. import json
  7. import os
  8. import secrets
  9. import socket
  10. import struct
  11. import sys
  12. import time
  13. from dataclasses import asdict, dataclass
  14. from datetime import datetime, timezone
  15. from typing import Callable, Iterable


  16. DEFAULT_EPDG_HOST = "epdg.epc.mnc010.mcc234.pub.3gppnetwork.org"
  17. DEFAULT_DNS_TARGETS = (("dns_cloudflare", "1.1.1.1"), ("dns_google", "8.8.8.8"))

  18. # RFC 3526 MODP group 14.
  19. MODP_2048_PRIME = int(
  20.     "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
  21.     "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
  22.     "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
  23.     "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
  24.     "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
  25.     "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
  26.     "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
  27.     "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
  28.     "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
  29.     "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
  30.     "15728E5A8AACAA68FFFFFFFFFFFFFFFF",
  31.     16,
  32. )


  33. @dataclass
  34. class ProbeResult:
  35.     name: str
  36.     target: str
  37.     passed: bool
  38.     status: str
  39.     rtt_ms: float | None = None
  40.     bytes_received: int = 0
  41.     detail: str = ""


  42. def parse_proxy(value: str) -> tuple[str, int]:
  43.     host, separator, raw_port = value.rpartition(":")
  44.     if not separator or not host:
  45.         raise argparse.ArgumentTypeError("proxy must use HOST:PORT")
  46.     try:
  47.         port = int(raw_port)
  48.     except ValueError as error:
  49.         raise argparse.ArgumentTypeError("proxy port must be an integer") from error
  50.     if not 1 <= port <= 65535:
  51.         raise argparse.ArgumentTypeError("proxy port must be between 1 and 65535")
  52.     return host, port


  53. def recv_exact(sock: socket.socket, size: int) -> bytes:
  54.     chunks = bytearray()
  55.     while len(chunks) < size:
  56.         chunk = sock.recv(size - len(chunks))
  57.         if not chunk:
  58.             raise RuntimeError("SOCKS5 control connection closed")
  59.         chunks.extend(chunk)
  60.     return bytes(chunks)


  61. def encode_socks_udp(ip: str, port: int, payload: bytes) -> bytes:
  62.     return b"\x00\x00\x00\x01" + socket.inet_aton(ip) + struct.pack("!H", port) + payload


  63. def decode_socks_udp(frame: bytes) -> tuple[str, int, bytes]:
  64.     if len(frame) < 10 or frame[:3] != b"\x00\x00\x00":
  65.         raise ValueError("invalid SOCKS5 UDP frame")
  66.     address_type = frame[3]
  67.     offset = 4
  68.     if address_type == 1:
  69.         host = socket.inet_ntoa(frame[offset : offset + 4])
  70.         offset += 4
  71.     elif address_type == 3:
  72.         name_length = frame[offset]
  73.         offset += 1
  74.         host = frame[offset : offset + name_length].decode("ascii")
  75.         offset += name_length
  76.     elif address_type == 4:
  77.         host = socket.inet_ntop(socket.AF_INET6, frame[offset : offset + 16])
  78.         offset += 16
  79.     else:
  80.         raise ValueError(f"unsupported SOCKS5 address type: {address_type}")
  81.     if len(frame) < offset + 2:
  82.         raise ValueError("truncated SOCKS5 UDP frame")
  83.     port = struct.unpack("!H", frame[offset : offset + 2])[0]
  84.     return host, port, frame[offset + 2 :]


  85. class Socks5UdpClient:
  86.     def __init__(self, proxy: tuple[str, int], timeout: float):
  87.         self.proxy = proxy
  88.         self.timeout = timeout
  89.         self.control: socket.socket | None = None
  90.         self.udp: socket.socket | None = None
  91.         self.relay: tuple[str, int] | None = None

  92.     def __enter__(self) -> "Socks5UdpClient":
  93.         self.udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  94.         self.udp.bind(("127.0.0.1", 0))
  95.         local_host, local_port = self.udp.getsockname()

  96.         self.control = socket.create_connection(self.proxy, timeout=self.timeout)
  97.         self.control.settimeout(self.timeout)
  98.         self.control.sendall(b"\x05\x01\x00")
  99.         if recv_exact(self.control, 2) != b"\x05\x00":
  100.             raise RuntimeError("SOCKS5 proxy requires unsupported authentication")

  101.         request = (
  102.             b"\x05\x03\x00\x01"
  103.             + socket.inet_aton(local_host)
  104.             + struct.pack("!H", local_port)
  105.         )
  106.         self.control.sendall(request)
  107.         header = recv_exact(self.control, 4)
  108.         if header[:2] != b"\x05\x00":
  109.             raise RuntimeError(f"SOCKS5 UDP ASSOCIATE failed with code {header[1]}")
  110.         relay_host = self._read_address(header[3])
  111.         relay_port = struct.unpack("!H", recv_exact(self.control, 2))[0]
  112.         if relay_host in ("0.0.0.0", "::"):
  113.             relay_host = self.proxy[0]
  114.         self.relay = (relay_host, relay_port)
  115.         return self

  116.     def _read_address(self, address_type: int) -> str:
  117.         assert self.control is not None
  118.         if address_type == 1:
  119.             return socket.inet_ntoa(recv_exact(self.control, 4))
  120.         if address_type == 3:
  121.             length = recv_exact(self.control, 1)[0]
  122.             return recv_exact(self.control, length).decode("ascii")
  123.         if address_type == 4:
  124.             return socket.inet_ntop(socket.AF_INET6, recv_exact(self.control, 16))
  125.         raise RuntimeError(f"unsupported SOCKS5 relay address type: {address_type}")

  126.     def request(
  127.         self,
  128.         target_ip: str,
  129.         target_port: int,
  130.         payload: bytes,
  131.         validator: Callable[[bytes], bool],
  132.     ) -> ProbeResult:
  133.         assert self.udp is not None and self.relay is not None
  134.         self.udp.settimeout(self.timeout)
  135.         started = time.monotonic()
  136.         self.udp.sendto(encode_socks_udp(target_ip, target_port, payload), self.relay)
  137.         deadline = started + self.timeout
  138.         while True:
  139.             remaining = deadline - time.monotonic()
  140.             if remaining <= 0:
  141.                 return ProbeResult(
  142.                     name="udp",
  143.                     target=f"{target_ip}:{target_port}",
  144.                     passed=False,
  145.                     status="timeout",
  146.                 )
  147.             self.udp.settimeout(remaining)
  148.             try:
  149.                 frame, _ = self.udp.recvfrom(65535)
  150.             except socket.timeout:
  151.                 return ProbeResult(
  152.                     name="udp",
  153.                     target=f"{target_ip}:{target_port}",
  154.                     passed=False,
  155.                     status="timeout",
  156.                 )
  157.             try:
  158.                 _, _, response = decode_socks_udp(frame)
  159.             except ValueError:
  160.                 continue
  161.             if not validator(response):
  162.                 continue
  163.             return ProbeResult(
  164.                 name="udp",
  165.                 target=f"{target_ip}:{target_port}",
  166.                 passed=True,
  167.                 status="reply",
  168.                 rtt_ms=round((time.monotonic() - started) * 1000, 1),
  169.                 bytes_received=len(response),
  170.             )

  171.     def request_many_ike(
  172.         self, targets: Iterable[str], port: int
  173.     ) -> list[ProbeResult]:
  174.         assert self.udp is not None and self.relay is not None
  175.         pending: dict[bytes, tuple[str, float]] = {}
  176.         marker = port == 4500
  177.         source_port = self.udp.getsockname()[1]
  178.         for target_ip in targets:
  179.             initiator_spi, packet = build_ikev2_request(
  180.                 target_ip, port, marker, source_port
  181.             )
  182.             pending[initiator_spi] = (target_ip, time.monotonic())
  183.             self.udp.sendto(encode_socks_udp(target_ip, port, packet), self.relay)

  184.         results: dict[str, ProbeResult] = {}
  185.         deadline = time.monotonic() + self.timeout
  186.         while pending and time.monotonic() < deadline:
  187.             self.udp.settimeout(max(0.1, deadline - time.monotonic()))
  188.             try:
  189.                 frame, _ = self.udp.recvfrom(65535)
  190.             except socket.timeout:
  191.                 break
  192.             try:
  193.                 _, _, response = decode_socks_udp(frame)
  194.             except ValueError:
  195.                 continue
  196.             ike = response[4:] if response.startswith(b"\x00\x00\x00\x00") else response
  197.             parsed = parse_ikev2_response(ike)
  198.             if parsed is None:
  199.                 continue
  200.             initiator_spi, valid = parsed
  201.             if initiator_spi not in pending:
  202.                 continue
  203.             target_ip, started = pending.pop(initiator_spi)
  204.             results[target_ip] = ProbeResult(
  205.                 name=f"ikev2_udp{port}",
  206.                 target=f"{target_ip}:{port}",
  207.                 passed=valid,
  208.                 status="valid_ikev2_reply" if valid else "invalid_ikev2_reply",
  209.                 rtt_ms=round((time.monotonic() - started) * 1000, 1),
  210.                 bytes_received=len(ike),
  211.             )

  212.         for target_ip, _ in pending.values():
  213.             results[target_ip] = ProbeResult(
  214.                 name=f"ikev2_udp{port}",
  215.                 target=f"{target_ip}:{port}",
  216.                 passed=False,
  217.                 status="timeout",
  218.             )
  219.         return [results[target_ip] for target_ip in targets]

  220.     def __exit__(self, *_: object) -> None:
  221.         if self.udp is not None:
  222.             self.udp.close()
  223.         if self.control is not None:
  224.             self.control.close()


  225. def socks5_tcp_connect(proxy: tuple[str, int], timeout: float) -> ProbeResult:
  226.     started = time.monotonic()
  227.     try:
  228.         with socket.create_connection(proxy, timeout=timeout) as control:
  229.             control.settimeout(timeout)
  230.             control.sendall(b"\x05\x01\x00")
  231.             if recv_exact(control, 2) != b"\x05\x00":
  232.                 raise RuntimeError("authentication required")
  233.             host = b"www.cloudflare.com"
  234.             request = b"\x05\x01\x00\x03" + bytes([len(host)]) + host + struct.pack("!H", 443)
  235.             control.sendall(request)
  236.             header = recv_exact(control, 4)
  237.             if header[:2] != b"\x05\x00":
  238.                 raise RuntimeError(f"CONNECT failed with code {header[1]}")
  239.             address_type = header[3]
  240.             if address_type == 1:
  241.                 recv_exact(control, 4)
  242.             elif address_type == 3:
  243.                 recv_exact(control, recv_exact(control, 1)[0])
  244.             elif address_type == 4:
  245.                 recv_exact(control, 16)
  246.             else:
  247.                 raise RuntimeError("invalid CONNECT reply")
  248.             recv_exact(control, 2)
  249.         return ProbeResult(
  250.             name="socks5_tcp_connect",
  251.             target="www.cloudflare.com:443",
  252.             passed=True,
  253.             status="connected",
  254.             rtt_ms=round((time.monotonic() - started) * 1000, 1),
  255.         )
  256.     except (OSError, RuntimeError) as error:
  257.         return ProbeResult(
  258.             name="socks5_tcp_connect",
  259.             target="www.cloudflare.com:443",
  260.             passed=False,
  261.             status="failed",
  262.             detail=str(error),
  263.         )


  264. def build_dns_query(name: str = "example.com") -> tuple[bytes, Callable[[bytes], bool]]:
  265.     transaction_id = os.urandom(2)
  266.     qname = b"".join(bytes([len(label)]) + label.encode("ascii") for label in name.split(".")) + b"\x00"
  267.     packet = (
  268.         transaction_id
  269.         + struct.pack("!HHHHH", 0x0100, 1, 0, 0, 0)
  270.         + qname
  271.         + struct.pack("!HH", 1, 1)
  272.     )

  273.     def validate(response: bytes) -> bool:
  274.         return (
  275.             len(response) >= 12
  276.             and response[:2] == transaction_id
  277.             and bool(response[2] & 0x80)
  278.         )

  279.     return packet, validate


  280. def build_stun_query() -> tuple[bytes, Callable[[bytes], bool]]:
  281.     transaction_id = os.urandom(12)
  282.     packet = struct.pack("!HHI12s", 0x0001, 0, 0x2112A442, transaction_id)

  283.     def validate(response: bytes) -> bool:
  284.         return (
  285.             len(response) >= 20
  286.             and response[:2] == b"\x01\x01"
  287.             and response[4:8] == b"\x21\x12\xa4\x42"
  288.             and response[8:20] == transaction_id
  289.         )

  290.     return packet, validate


  291. def ike_transform(
  292.     more: bool, transform_type: int, transform_id: int, attributes: bytes = b""
  293. ) -> bytes:
  294.     return struct.pack(
  295.         "!BBHBBH",
  296.         3 if more else 0,
  297.         0,
  298.         8 + len(attributes),
  299.         transform_type,
  300.         0,
  301.         transform_id,
  302.     ) + attributes


  303. def ike_notify(next_payload: int, notify_type: int, data: bytes = b"") -> bytes:
  304.     body = struct.pack("!BBH", 0, 0, notify_type) + data
  305.     return struct.pack("!BBH", next_payload, 0, 4 + len(body)) + body


  306. def nat_detection_hash(initiator_spi: bytes, ip: str, port: int) -> bytes:
  307.     return hashlib.sha1(
  308.         initiator_spi
  309.         + b"\x00" * 8
  310.         + socket.inet_aton(ip)
  311.         + struct.pack("!H", port)
  312.     ).digest()


  313. def build_ikev2_request(
  314.     target_ip: str,
  315.     target_port: int,
  316.     marker: bool,
  317.     source_port: int = 45000,
  318. ) -> tuple[bytes, bytes]:
  319.     initiator_spi = os.urandom(8)
  320.     transforms = b"".join(
  321.         (
  322.             ike_transform(True, 1, 12, struct.pack("!HH", 0x800E, 256)),
  323.             ike_transform(True, 2, 5),
  324.             ike_transform(True, 3, 12),
  325.             ike_transform(False, 4, 14),
  326.         )
  327.     )
  328.     proposal = struct.pack(
  329.         "!BBHBBBB", 0, 0, 8 + len(transforms), 1, 1, 0, 4
  330.     ) + transforms
  331.     security_association = struct.pack("!BBH", 34, 0, 4 + len(proposal)) + proposal
  332.     public_value = pow(
  333.         2, secrets.randbits(320) | 1, MODP_2048_PRIME
  334.     ).to_bytes(256, "big")
  335.     key_exchange = struct.pack("!BBHHH", 40, 0, 264, 14, 0) + public_value
  336.     nonce_data = os.urandom(32)
  337.     nonce = struct.pack("!BBH", 41, 0, 4 + len(nonce_data)) + nonce_data
  338.     source_nat = ike_notify(
  339.         41, 16388, nat_detection_hash(initiator_spi, "127.0.0.1", source_port)
  340.     )
  341.     destination_nat = ike_notify(
  342.         41, 16389, nat_detection_hash(initiator_spi, target_ip, target_port)
  343.     )
  344.     fragmentation = ike_notify(0, 16430)
  345.     body = security_association + key_exchange + nonce + source_nat + destination_nat + fragmentation
  346.     header = struct.pack(
  347.         "!8s8sBBBBII",
  348.         initiator_spi,
  349.         b"\x00" * 8,
  350.         33,
  351.         0x20,
  352.         34,
  353.         0x08,
  354.         0,
  355.         28 + len(body),
  356.     )
  357.     packet = header + body
  358.     return initiator_spi, (b"\x00\x00\x00\x00" + packet if marker else packet)


  359. def parse_ikev2_response(packet: bytes) -> tuple[bytes, bool] | None:
  360.     if len(packet) < 28:
  361.         return None
  362.     fields = struct.unpack("!8s8sBBBBII", packet[:28])
  363.     initiator_spi, responder_spi, _, version, exchange, flags, _, length = fields
  364.     valid = (
  365.         responder_spi != b"\x00" * 8
  366.         and version >> 4 == 2
  367.         and exchange == 34
  368.         and bool(flags & 0x20)
  369.         and 28 <= length <= len(packet)
  370.     )
  371.     return initiator_spi, bool(valid)


  372. def resolve_ipv4(host: str) -> list[str]:
  373.     return sorted(
  374.         {
  375.             item[4][0]
  376.             for item in socket.getaddrinfo(host, None, socket.AF_INET, socket.SOCK_DGRAM)
  377.         }
  378.     )


  379. def classify(results: list[ProbeResult]) -> str:
  380.     ike500 = [result for result in results if result.name == "ikev2_udp500"]
  381.     ike4500 = [result for result in results if result.name == "ikev2_udp4500"]
  382.     if not any(result.passed for result in ike500) or not any(
  383.         result.passed for result in ike4500
  384.     ):
  385.         return "FAIL"
  386.     if not all(result.passed for result in ike500 + ike4500):
  387.         return "PARTIAL"
  388.     return "PASS"


  389. def run(args: argparse.Namespace) -> tuple[list[ProbeResult], list[str]]:
  390.     results = [socks5_tcp_connect(args.proxy, args.timeout)]
  391.     resolved_epdg_ips = args.epdg_ip or resolve_ipv4(args.epdg_host)
  392.     epdg_ips = (
  393.         resolved_epdg_ips
  394.         if args.epdg_ip or args.all_epdg
  395.         else resolved_epdg_ips[:1]
  396.     )

  397.     # Keep control traffic and each IKE port in separate UDP associations.
  398.     # Some proxy chains retain destination-specific UDP state after a timeout.
  399.     with Socks5UdpClient(args.proxy, args.timeout) as client:
  400.         for name, target_ip in DEFAULT_DNS_TARGETS:
  401.             packet, validator = build_dns_query()
  402.             result = client.request(target_ip, 53, packet, validator)
  403.             result.name = name
  404.             results.append(result)

  405.         stun_ip = socket.gethostbyname("stun.l.google.com")
  406.         packet, validator = build_stun_query()
  407.         result = client.request(stun_ip, 19302, packet, validator)
  408.         result.name = "stun_google"
  409.         result.detail = "informational; not a WiFi Calling gate"
  410.         results.append(result)

  411.     with Socks5UdpClient(args.proxy, args.timeout) as client:
  412.         results.extend(client.request_many_ike(epdg_ips, 500))

  413.     with Socks5UdpClient(args.proxy, args.timeout) as client:
  414.         results.extend(client.request_many_ike(epdg_ips, 4500))
  415.     return results, epdg_ips


  416. def format_text(
  417.     args: argparse.Namespace, results: list[ProbeResult], epdg_ips: list[str]
  418. ) -> str:
  419.     lines = [
  420.         f"Proxy: {args.proxy[0]}:{args.proxy[1]}",
  421.         f"Node: {args.node_label or '(not specified)'}",
  422.         f"ePDG: {args.epdg_host} -> {', '.join(epdg_ips)}",
  423.         "",
  424.     ]
  425.     for result in results:
  426.         state = "PASS" if result.passed else "FAIL"
  427.         timing = f" {result.rtt_ms:.1f}ms" if result.rtt_ms is not None else ""
  428.         lines.append(
  429.             f"[{state}] {result.name:<20} {result.target:<24} {result.status}{timing}"
  430.         )
  431.     verdict = classify(results)
  432.     lines.extend(
  433.         (
  434.             "",
  435.             f"Verdict: {verdict}",
  436.             "PASS proves pre-auth IKE reachability, not carrier account/SIM authentication.",
  437.         )
  438.     )
  439.     return "\n".join(lines)


  440. def format_json(
  441.     args: argparse.Namespace, results: list[ProbeResult], epdg_ips: list[str]
  442. ) -> str:
  443.     payload = {
  444.         "timestamp_utc": datetime.now(timezone.utc).isoformat(),
  445.         "proxy": f"{args.proxy[0]}:{args.proxy[1]}",
  446.         "node_label": args.node_label or None,
  447.         "epdg_host": args.epdg_host,
  448.         "epdg_ips": epdg_ips,
  449.         "verdict": classify(results),
  450.         "results": [asdict(result) for result in results],
  451.         "caveat": "IKE probes verify pre-auth reachability, not carrier account/SIM authentication.",
  452.     }
  453.     return json.dumps(payload, ensure_ascii=False, indent=2)


  454. def format_ticket(
  455.     args: argparse.Namespace, results: list[ProbeResult], epdg_ips: list[str]
  456. ) -> str:
  457.     verdict = classify(results)
  458.     failed = [result for result in results if not result.passed]
  459.     passed = [result for result in results if result.passed]
  460.     lines = [
  461.         "Subject: UDP relay / WiFi Calling IKE reachability issue",
  462.         "",
  463.         f"Test time (UTC): {datetime.now(timezone.utc).isoformat()}",
  464.         f"Node: {args.node_label or '[fill in node name]'}",
  465.         f"Local proxy: SOCKS5 {args.proxy[0]}:{args.proxy[1]}",
  466.         f"Target: {args.epdg_host} ({', '.join(epdg_ips)})",
  467.         f"Automated verdict: {verdict}",
  468.         "",
  469.         "Expected:",
  470.         "The node should relay UDP request/response traffic, including IKEv2 UDP 500 and NAT-T UDP 4500.",
  471.         "",
  472.         "Passed checks:",
  473.     ]
  474.     lines.extend(
  475.         f"- {result.name} {result.target}: {result.status}"
  476.         for result in passed
  477.     )
  478.     lines.append("")
  479.     lines.append("Failed checks:")
  480.     lines.extend(
  481.         f"- {result.name} {result.target}: {result.status}"
  482.         for result in failed
  483.     )
  484.     if not failed:
  485.         lines.append("- None")
  486.     lines.extend(
  487.         (
  488.             "",
  489.             "Request:",
  490.             "Please verify server-side UDP relay, firewall rules, NAT mappings, and outbound UDP 500/4500 for this node.",
  491.             "",
  492.             "Note: STUN is informational. IKE results are valid protocol responses but do not test carrier account/SIM authentication.",
  493.         )
  494.     )
  495.     return "\n".join(lines)


  496. def build_parser() -> argparse.ArgumentParser:
  497.     parser = argparse.ArgumentParser(
  498.         description="Test a local SOCKS5 node for UDP DNS, STUN, and WiFi Calling IKE reachability."
  499.     )
  500.     parser.add_argument(
  501.         "--proxy",
  502.         required=True,
  503.         type=parse_proxy,
  504.         metavar="HOST:PORT",
  505.         help="local no-auth SOCKS5 or Mihomo mixed listener",
  506.     )
  507.     parser.add_argument("--node-label", default="", help="node name included in reports")
  508.     parser.add_argument("--epdg-host", default=DEFAULT_EPDG_HOST)
  509.     parser.add_argument(
  510.         "--epdg-ip",
  511.         action="append",
  512.         default=[],
  513.         help="override resolved ePDG IPv4 address; repeat for multiple addresses",
  514.     )
  515.     parser.add_argument(
  516.         "--all-epdg",
  517.         action="store_true",
  518.         help="probe every resolved ePDG address instead of one address",
  519.     )
  520.     parser.add_argument("--timeout", type=float, default=4.0, help="timeout per probe in seconds")
  521.     parser.add_argument(
  522.         "--format", choices=("text", "json", "ticket"), default="text"
  523.     )
  524.     return parser


  525. def main() -> int:
  526.     args = build_parser().parse_args()
  527.     try:
  528.         results, epdg_ips = run(args)
  529.     except (OSError, RuntimeError) as error:
  530.         print(f"Probe setup failed: {error}", file=sys.stderr)
  531.         return 1
  532.     if not epdg_ips:
  533.         print("No ePDG IPv4 addresses resolved", file=sys.stderr)
  534.         return 1

  535.     if args.format == "json":
  536.         print(format_json(args, results, epdg_ips))
  537.     elif args.format == "ticket":
  538.         print(format_ticket(args, results, epdg_ips))
  539.     else:
  540.         print(format_text(args, results, epdg_ips))

  541.     verdict = classify(results)
  542.     return 0 if verdict == "PASS" else 2 if verdict == "PARTIAL" else 1


  543. if __name__ == "__main__":
  544.     raise SystemExit(main())
复制代码
爱生活,爱奶昔~
CRENETLv.2 发表于 2026-8-1 06:28:54 | 查看全部
Android拉起WIFICalling其实挺轻松的,参考我主页的两篇主题帖子
昨天上午九点花了不到10分钟成功拉起CTEUK的WIFICalling并且激活
爱生活,爱奶昔~
回复 支持 1 反对 0

使用道具 举报

uu233Lv.2 发表于 2026-8-1 06:18:27 | 查看全部
tudouy 发表于 2026-7-30 23:44
我用小火箭拉起的WiFi Calling就是这样,  试试软路由或电脑手机开启全局WiFi 看看 ...

感谢回复,换了新的节点+gpt自动操作终于弄好了,但888显示无人接听hh
爱生活,爱奶昔~
回复 支持 1 反对 0

使用道具 举报

XiziLv.3 发表于 2026-7-31 01:24:41 | 查看全部
可以,还算懂点,顺便说一下500是认证端口,4500是nat穿越端口
爱生活,爱奶昔~
回复 支持 1 反对 0

使用道具 举报

随便点Lv.1 发表于 2026-7-30 23:05:09 | 查看全部
我试验的过程中,还有个办法就是先配置好小飞机连接电脑的热点,但小飞机本身不开机场代理,只连接电脑路由。

电脑端的代理用CLASH VERGE开全局,不开TUN。

用CODEX写好UDP 4500和500配置文件导入小飞机。

再打开飞行模式,重复用IOS手机WIFI开关就行了,等wifi calling出现。

唯一的问题,就是打电话888不通的问题还没解决……
配置文件怎么弄,能分享吗 
发表于 2026-7-31 11:39
爱生活,爱奶昔~
回复 支持 1 反对 0

使用道具 举报

tudouy楼主Lv.2 发表于 2026-7-30 22:44:38 | 查看全部
amigodoreen 发表于 2026-7-30 22:39
我的天,你说的这些我全都踩过坑,现在一看你说的简直句句精髓

谁能想到是UDP的问题😂
爱生活,爱奶昔~
回复 支持 1 反对 0

使用道具 举报

阿根Lv.4 发表于 2026-7-30 22:36:11 来自手机 | 查看全部
终于遇到个聪明人🤓
爱生活,爱奶昔~
tudouy楼主Lv.2 发表于 2026-7-30 22:38:51 | 查看全部
阿根 发表于 2026-7-30 22:36
终于遇到个聪明人🤓

确实难拉😂 前前后后花了不少时间。也是让Codex帮我查日志才发现UDP端口有问题。
爱生活,爱奶昔~
amigodoreenLv.2 发表于 2026-7-30 22:39:29 来自手机 | 查看全部
我的天,你说的这些我全都踩过坑,现在一看你说的简直句句精髓
爱生活,爱奶昔~
希酱粉丝Lv.2 发表于 2026-7-30 22:39:57 | 查看全部
感谢,我也用chatgpt研究研究
爱生活,爱奶昔~
tudouy楼主Lv.2 发表于 2026-7-30 22:45:17 | 查看全部
希酱粉丝 发表于 2026-7-30 22:39
感谢,我也用chatgpt研究研究


哈哈还是得上Codex. 用软路由直接Codex ssh 连上openwrt去查日志很方便。
爱生活,爱奶昔~
HOPELv.1 发表于 2026-7-30 22:56:21 | 查看全部
我试了iphone15,一定要重启手机,重启前把wificall关掉,启动后先连电脑wifi再打开,pc的以太网的ipv6一定要关掉
爱生活,爱奶昔~
tudouy楼主Lv.2 发表于 2026-7-30 23:00:57 | 查看全部
HOPE 发表于 2026-7-30 22:56
我试了iphone15,一定要重启手机,重启前把wificall关掉,启动后先连电脑wifi再打开,pc的以太网的ipv6一定 ...

哦对,ipv6 要关掉,只能用ipv4,这个我忘记提了。 我后面用二手的 SE3 + 软路由就比较顺畅,不需要重启开飞行模式就行
爱生活,爱奶昔~
uu233Lv.2 发表于 2026-7-30 23:14:08 | 查看全部
尝试了很久,能wifi calling但没办法打给888
爱生活,爱奶昔~
KevinPDLv.2 发表于 2026-7-30 23:20:07 | 查看全部
很有用,收藏了
爱生活,爱奶昔~
tudouy楼主Lv.2 发表于 2026-7-30 23:44:28 | 查看全部
uu233 发表于 2026-7-30 23:14
尝试了很久,能wifi calling但没办法打给888

我用小火箭拉起的WiFi Calling就是这样,  试试软路由或电脑手机开启全局WiFi 看看
爱生活,爱奶昔~
tudouy楼主Lv.2 发表于 2026-7-30 23:46:38 | 查看全部
随便点 发表于 2026-7-30 23:05
我试验的过程中,还有个办法就是先配置好小飞机连接电脑的热点,但小飞机本身不开机场代理,只连接电脑路由 ...

我是小飞机开隧道也是打电话888不通后面换软路由才打通888的。
爱生活,爱奶昔~
tudouy楼主Lv.2 发表于 2026-7-30 23:46:56 | 查看全部
KevinPD 发表于 2026-7-30 23:20
很有用,收藏了

有帮助就好
爱生活,爱奶昔~
随便点Lv.1 发表于 2026-7-30 23:53:58 | 查看全部
tudouy 发表于 2026-7-30 23:46
我是小飞机开隧道也是打电话888不通后面换软路由才打通888的。

没有软路由的咋办?
爱生活,爱奶昔~
tudouy楼主Lv.2 发表于 2026-7-30 23:56:18 | 查看全部
随便点 发表于 2026-7-30 23:53
没有软路由的咋办?

可以海鲜市场淘一个。
或者用电脑或手机开全局热点方案,这个我就没尝试过了~ 站内也有一些教程。
爱生活,爱奶昔~
nqweLv.2 发表于 2026-7-31 03:54:13 | 查看全部
测试脚本可以分享一下吗,十分需要了
爱生活,爱奶昔~
茶拿铁Lv.4 发表于 2026-7-31 04:11:32 | 查看全部
所以如果VPS自建,打开UDP 500 和 4500 端口就能拉起了吗?
爱生活,爱奶昔~

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则

© 2026 Naixi Networks. 沪ICP备13020230号-1|沪公网安备 31010702007642号手机版小黑屋RSS
返回顶部 关灯 在本版发帖
快速回复 返回顶部 返回列表