mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-15 10:40:28 +08:00
rewrited stun
This commit is contained in:
parent
787e5ddbae
commit
bed223bd86
@ -661,9 +661,11 @@ def _get_config_scheme() -> Dict:
|
|||||||
|
|
||||||
"janus": {
|
"janus": {
|
||||||
"stun": {
|
"stun": {
|
||||||
"host": Option("stun.l.google.com", type=valid_ip_or_host, unpack_as="stun_host"),
|
"host": Option("stun.l.google.com", type=valid_ip_or_host, unpack_as="stun_host"),
|
||||||
"port": Option(19302, type=valid_port, unpack_as="stun_port"),
|
"port": Option(19302, type=valid_port, unpack_as="stun_port"),
|
||||||
"timeout": Option(5.0, type=valid_float_f01, unpack_as="stun_timeout"),
|
"timeout": Option(5.0, type=valid_float_f01, unpack_as="stun_timeout"),
|
||||||
|
"retries": Option(5, type=valid_int_f1, unpack_as="stun_retries"),
|
||||||
|
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="stun_retries_delay"),
|
||||||
},
|
},
|
||||||
|
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from ... import aioproc
|
|||||||
|
|
||||||
from ...logging import get_logger
|
from ...logging import get_logger
|
||||||
|
|
||||||
from .stun import stun_get_info
|
from .stun import Stun
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
@ -35,6 +35,8 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
stun_host: str,
|
stun_host: str,
|
||||||
stun_port: int,
|
stun_port: int,
|
||||||
stun_timeout: float,
|
stun_timeout: float,
|
||||||
|
stun_retries: int,
|
||||||
|
stun_retries_delay: float,
|
||||||
|
|
||||||
check_interval: int,
|
check_interval: int,
|
||||||
check_retries: int,
|
check_retries: int,
|
||||||
@ -45,9 +47,7 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
cmd_append: List[str],
|
cmd_append: List[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.__stun_host = stun_host
|
self.__stun = Stun(stun_host, stun_port, stun_timeout, stun_retries, stun_retries_delay)
|
||||||
self.__stun_port = stun_port
|
|
||||||
self.__stun_timeout = stun_timeout
|
|
||||||
|
|
||||||
self.__check_interval = check_interval
|
self.__check_interval = check_interval
|
||||||
self.__check_retries = check_retries
|
self.__check_retries = check_retries
|
||||||
@ -74,12 +74,15 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
try:
|
try:
|
||||||
prev_netcfg: Optional[_Netcfg] = None
|
prev_netcfg: Optional[_Netcfg] = None
|
||||||
while True:
|
while True:
|
||||||
|
retry = 0
|
||||||
netcfg = _Netcfg()
|
netcfg = _Netcfg()
|
||||||
for _ in range(self.__check_retries - 1):
|
for retry in range(self.__check_retries):
|
||||||
netcfg = await self.__get_netcfg()
|
netcfg = await self.__get_netcfg()
|
||||||
if netcfg.ext_ip:
|
if netcfg.ext_ip:
|
||||||
break
|
break
|
||||||
await asyncio.sleep(self.__check_retries_delay)
|
await asyncio.sleep(self.__check_retries_delay)
|
||||||
|
if retry != 0 and netcfg.ext_ip:
|
||||||
|
logger.info("I'm fine, continue working ...")
|
||||||
|
|
||||||
if netcfg != prev_netcfg:
|
if netcfg != prev_netcfg:
|
||||||
logger.info("Got new %s", netcfg)
|
logger.info("Got new %s", netcfg)
|
||||||
@ -99,8 +102,8 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
async def __get_netcfg(self) -> _Netcfg:
|
async def __get_netcfg(self) -> _Netcfg:
|
||||||
src_ip = (self.__get_default_ip() or "0.0.0.0")
|
src_ip = (self.__get_default_ip() or "0.0.0.0")
|
||||||
(nat_type, ext_ip) = await self.__get_stun_info(src_ip)
|
(stun, (nat_type, ext_ip)) = await self.__get_stun_info(src_ip)
|
||||||
return _Netcfg(nat_type, src_ip, ext_ip, self.__stun_host, self.__stun_port)
|
return _Netcfg(nat_type, src_ip, ext_ip, stun.host, stun.port)
|
||||||
|
|
||||||
def __get_default_ip(self) -> str:
|
def __get_default_ip(self) -> str:
|
||||||
try:
|
try:
|
||||||
@ -122,18 +125,12 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
get_logger().error("Can't get default IP: %s", tools.efmt(err))
|
get_logger().error("Can't get default IP: %s", tools.efmt(err))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def __get_stun_info(self, src_ip: str) -> Tuple[str, str]:
|
async def __get_stun_info(self, src_ip: str) -> Tuple[Stun, Tuple[str, str]]:
|
||||||
try:
|
try:
|
||||||
return (await stun_get_info(
|
return (self.__stun, (await self.__stun.get_info(src_ip, 0)))
|
||||||
stun_host=self.__stun_host,
|
|
||||||
stun_port=self.__stun_port,
|
|
||||||
src_ip=src_ip,
|
|
||||||
src_port=0,
|
|
||||||
timeout=self.__stun_timeout,
|
|
||||||
))
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
get_logger().error("Can't get STUN info: %s", tools.efmt(err))
|
get_logger().error("Can't get STUN info: %s", tools.efmt(err))
|
||||||
return ("", "")
|
return (self.__stun, ("", ""))
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import secrets
|
import secrets
|
||||||
@ -40,144 +41,137 @@ class StunNatType:
|
|||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
async def stun_get_info(
|
class Stun:
|
||||||
stun_host: str,
|
|
||||||
stun_port: int,
|
|
||||||
src_ip: str,
|
|
||||||
src_port: int,
|
|
||||||
timeout: float,
|
|
||||||
) -> Tuple[str, str]:
|
|
||||||
|
|
||||||
return (await aiotools.run_async(_stun_get_info, stun_host, stun_port, src_ip, src_port, timeout))
|
|
||||||
|
|
||||||
|
|
||||||
def _stun_get_info(
|
|
||||||
stun_host: str,
|
|
||||||
stun_port: int,
|
|
||||||
src_ip: str,
|
|
||||||
src_port: int,
|
|
||||||
timeout: float,
|
|
||||||
) -> Tuple[str, str]:
|
|
||||||
|
|
||||||
# Partially based on https://github.com/JohnVillalovos/pystun
|
# Partially based on https://github.com/JohnVillalovos/pystun
|
||||||
|
|
||||||
(family, _, _, _, addr) = socket.getaddrinfo(src_ip, src_port, type=socket.SOCK_DGRAM)[0]
|
def __init__(
|
||||||
with socket.socket(family, socket.SOCK_DGRAM) as sock:
|
self,
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
host: str,
|
||||||
sock.settimeout(timeout)
|
port: int,
|
||||||
sock.bind(addr)
|
timeout: float,
|
||||||
(nat_type, response) = _get_nat_type(
|
retries: int,
|
||||||
stun_host=stun_host,
|
retries_delay: float,
|
||||||
stun_port=stun_port,
|
) -> None:
|
||||||
src_ip=src_ip,
|
|
||||||
sock=sock,
|
|
||||||
)
|
|
||||||
return (nat_type, (response.ext.ip if response.ext is not None else ""))
|
|
||||||
|
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.__timeout = timeout
|
||||||
|
self.__retries = retries
|
||||||
|
self.__retries_delay = retries_delay
|
||||||
|
|
||||||
def _get_nat_type( # pylint: disable=too-many-return-statements
|
self.__sock: Optional[socket.socket] = None
|
||||||
stun_host: str,
|
|
||||||
stun_port: int,
|
|
||||||
src_ip: str,
|
|
||||||
sock: socket.socket,
|
|
||||||
) -> Tuple[str, StunResponse]:
|
|
||||||
|
|
||||||
first = _stun_request("First probe", stun_host, stun_port, b"", sock)
|
async def get_info(self, src_ip: str, src_port: int) -> Tuple[str, str]:
|
||||||
if not first.ok:
|
|
||||||
return (StunNatType.BLOCKED, first)
|
|
||||||
if first.ext is None:
|
|
||||||
raise RuntimeError(f"Ext addr is None: {first}")
|
|
||||||
|
|
||||||
request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000006) # Change-Request
|
(family, _, _, _, addr) = socket.getaddrinfo(src_ip, src_port, type=socket.SOCK_DGRAM)[0]
|
||||||
response = _stun_request("Change request [ext_ip == src_ip]", stun_host, stun_port, request, sock)
|
try:
|
||||||
|
with socket.socket(family, socket.SOCK_DGRAM) as self.__sock:
|
||||||
|
self.__sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.__sock.settimeout(self.__timeout)
|
||||||
|
self.__sock.bind(addr)
|
||||||
|
(nat_type, response) = await self.__get_nat_type(src_ip)
|
||||||
|
return (nat_type, (response.ext.ip if response.ext is not None else ""))
|
||||||
|
finally:
|
||||||
|
self.__sock = None
|
||||||
|
|
||||||
|
async def __get_nat_type(self, src_ip: str) -> Tuple[str, StunResponse]: # pylint: disable=too-many-return-statements
|
||||||
|
first = await self.__make_request("First probe")
|
||||||
|
if not first.ok:
|
||||||
|
return (StunNatType.BLOCKED, first)
|
||||||
|
|
||||||
|
request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000006) # Change-Request
|
||||||
|
response = await self.__make_request("Change request [ext_ip == src_ip]", request)
|
||||||
|
|
||||||
|
if first.ext is not None and first.ext.ip == src_ip:
|
||||||
|
if response.ok:
|
||||||
|
return (StunNatType.OPEN_INTERNET, response)
|
||||||
|
return (StunNatType.SYMMETRIC_UDP_FW, response)
|
||||||
|
|
||||||
if first.ext.ip == src_ip:
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
return (StunNatType.OPEN_INTERNET, response)
|
return (StunNatType.FULL_CONE_NAT, response)
|
||||||
return (StunNatType.SYMMETRIC_UDP_FW, response)
|
|
||||||
|
|
||||||
if response.ok:
|
if first.changed is None:
|
||||||
return (StunNatType.FULL_CONE_NAT, response)
|
raise RuntimeError(f"Changed addr is None: {first}")
|
||||||
|
response = await self.__make_request("Change request [ext_ip != src_ip]", b"", *first.changed.ip)
|
||||||
|
if not response.ok:
|
||||||
|
return (StunNatType.CHANGED_ADDR_ERROR, response)
|
||||||
|
|
||||||
if first.changed is None:
|
if response.ext == first.ext:
|
||||||
raise RuntimeError(f"Changed addr is None: {first}")
|
request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000002)
|
||||||
response = _stun_request("Change request [ext_ip != src_ip]", first.changed.ip, first.changed.port, b"", sock)
|
response = await self.__make_request("Change port", request, first.changed.ip)
|
||||||
if not response.ok:
|
if response.ok:
|
||||||
return (StunNatType.CHANGED_ADDR_ERROR, response)
|
return (StunNatType.RESTRICTED_NAT, response)
|
||||||
|
return (StunNatType.RESTRICTED_PORT_NAT, response)
|
||||||
|
|
||||||
if response.ext == first.ext:
|
return (StunNatType.SYMMETRIC_NAT, response)
|
||||||
request = struct.pack(">HHI", 0x0003, 0x0004, 0x00000002)
|
|
||||||
response = _stun_request("Change port", first.changed.ip, stun_port, request, sock)
|
|
||||||
if response.ok:
|
|
||||||
return (StunNatType.RESTRICTED_NAT, response)
|
|
||||||
return (StunNatType.RESTRICTED_PORT_NAT, response)
|
|
||||||
|
|
||||||
return (StunNatType.SYMMETRIC_NAT, response)
|
async def __make_request(self, ctx: str, request: bytes=b"", host: str="", port: int=0) -> StunResponse:
|
||||||
|
# TODO: Support IPv6 and RFC 5389
|
||||||
|
# The first 4 bytes of the response are the Type (2) and Length (2)
|
||||||
|
# The 5th byte is Reserved
|
||||||
|
# The 6th byte is the Family: 0x01 = IPv4, 0x02 = IPv6
|
||||||
|
# The remaining bytes are the IP address. 32 bits for IPv4 or 128 bits for
|
||||||
|
# IPv6.
|
||||||
|
# More info at: https://tools.ietf.org/html/rfc3489#section-11.2.1
|
||||||
|
# And at: https://tools.ietf.org/html/rfc5389#section-15.1
|
||||||
|
|
||||||
|
(response, error) = (b"", "")
|
||||||
|
for _ in range(self.__retries):
|
||||||
|
(response, error) = await self.__inner_make_request(request, host, port)
|
||||||
|
if not error:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(self.__retries_delay)
|
||||||
|
if error:
|
||||||
|
get_logger(0).error("%s: Can't perform STUN request after %d retries; last error: %s",
|
||||||
|
ctx, self.__retries, error)
|
||||||
|
return StunResponse(ok=False)
|
||||||
|
|
||||||
def _stun_request( # pylint: disable=too-many-locals
|
parsed: Dict[str, StunAddress] = {}
|
||||||
ctx: str,
|
offset = 0
|
||||||
host: str,
|
remaining = len(response)
|
||||||
port: int,
|
while remaining > 0:
|
||||||
request: bytes,
|
(attr_type, attr_len) = struct.unpack(">HH", response[offset : offset + 4]) # noqa: E203
|
||||||
sock: socket.socket,
|
offset += 4
|
||||||
) -> StunResponse:
|
field = {
|
||||||
|
0x0001: "ext", # MAPPED-ADDRESS
|
||||||
|
0x0004: "src", # SOURCE-ADDRESS
|
||||||
|
0x0005: "changed", # CHANGED-ADDRESS
|
||||||
|
}.get(attr_type)
|
||||||
|
if field is not None:
|
||||||
|
parsed[field] = self.__parse_address(response[offset:])
|
||||||
|
offset += attr_len
|
||||||
|
remaining -= (4 + attr_len)
|
||||||
|
return StunResponse(ok=True, **parsed)
|
||||||
|
|
||||||
# TODO: Support IPv6 and RFC 5389
|
async def __inner_make_request(self, request: bytes, host: str, port: int) -> Tuple[bytes, str]:
|
||||||
# The first 4 bytes of the response are the Type (2) and Length (2)
|
assert self.__sock is not None
|
||||||
# The 5th byte is Reserved
|
|
||||||
# The 6th byte is the Family: 0x01 = IPv4, 0x02 = IPv6
|
|
||||||
# The remaining bytes are the IP address. 32 bits for IPv4 or 128 bits for
|
|
||||||
# IPv6.
|
|
||||||
# More info at: https://tools.ietf.org/html/rfc3489#section-11.2.1
|
|
||||||
# And at: https://tools.ietf.org/html/rfc5389#section-15.1
|
|
||||||
|
|
||||||
trans_id = secrets.token_bytes(16)
|
trans_id = secrets.token_bytes(16)
|
||||||
request = struct.pack(">HH", 0x0001, len(request)) + trans_id + request # Bind Request
|
request = struct.pack(">HH", 0x0001, len(request)) + trans_id + request # Bind Request
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock.sendto(request, (host, port))
|
await aiotools.run_async(self.__sock.sendto, request, ((host or self.host), (port or self.port)))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
get_logger().error("%s: Can't send request: %s", ctx, tools.efmt(err))
|
return (b"", f"Send error: {tools.efmt(err)}")
|
||||||
return StunResponse(ok=False)
|
try:
|
||||||
try:
|
response = (await aiotools.run_async(self.__sock.recvfrom, 2048))[0]
|
||||||
response = sock.recvfrom(2048)[0]
|
except Exception as err:
|
||||||
except Exception as err:
|
return (b"", f"Recv error: {tools.efmt(err)}")
|
||||||
get_logger().error("%s: Can't recv response: %s", ctx, tools.efmt(err))
|
|
||||||
return StunResponse(ok=False)
|
|
||||||
|
|
||||||
(response_type, payload_len) = struct.unpack(">HH", response[:4])
|
(response_type, payload_len) = struct.unpack(">HH", response[:4])
|
||||||
if response_type != 0x0101:
|
if response_type != 0x0101:
|
||||||
get_logger().error("%s: Invalid response type: %#.4x", ctx, response_type)
|
return (b"", f"Invalid response type: {response_type:#06x}")
|
||||||
return StunResponse(ok=False)
|
if trans_id != response[4:20]:
|
||||||
if trans_id != response[4:20]:
|
return (b"", "Transaction ID mismatch")
|
||||||
get_logger().error("%s: Transaction ID mismatch")
|
|
||||||
return StunResponse(ok=False)
|
|
||||||
|
|
||||||
parsed: Dict[str, StunAddress] = {}
|
return (response[20 : 20 + payload_len], "") # noqa: E203
|
||||||
base = 20
|
|
||||||
remaining = payload_len
|
|
||||||
while remaining > 0:
|
|
||||||
(attr_type, attr_len) = struct.unpack(">HH", response[base:(base + 4)])
|
|
||||||
base += 4
|
|
||||||
field = {
|
|
||||||
0x0001: "ext", # MAPPED-ADDRESS
|
|
||||||
0x0004: "src", # SOURCE-ADDRESS
|
|
||||||
0x0005: "changed", # CHANGED-ADDRESS
|
|
||||||
}.get(attr_type)
|
|
||||||
if field is not None:
|
|
||||||
parsed[field] = _parse_address(response[base:])
|
|
||||||
base += attr_len
|
|
||||||
remaining -= (4 + attr_len)
|
|
||||||
return StunResponse(ok=True, **parsed)
|
|
||||||
|
|
||||||
|
def __parse_address(self, data: bytes) -> StunAddress:
|
||||||
def _parse_address(data: bytes) -> StunAddress:
|
family = data[1]
|
||||||
family = data[1]
|
if family == 1:
|
||||||
if family == 1:
|
parts = struct.unpack(">HBBBB", data[2:8])
|
||||||
parts = struct.unpack(">HBBBB", data[2:8])
|
return StunAddress(
|
||||||
return StunAddress(
|
ip=".".join(map(str, parts[1:])),
|
||||||
ip=".".join(map(str, parts[1:])),
|
port=parts[0],
|
||||||
port=parts[0],
|
)
|
||||||
)
|
raise RuntimeError(f"Only IPv4 supported; received: {family}")
|
||||||
raise RuntimeError(f"Only IPv4 supported; received: {family}")
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user