mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
janus runner draft
This commit is contained in:
parent
9cead62032
commit
19a68887e4
6
PKGBUILD
6
PKGBUILD
@ -48,6 +48,7 @@ depends=(
|
|||||||
python-spidev
|
python-spidev
|
||||||
python-setproctitle
|
python-setproctitle
|
||||||
python-psutil
|
python-psutil
|
||||||
|
python-netifaces
|
||||||
python-systemd
|
python-systemd
|
||||||
python-dbus
|
python-dbus
|
||||||
python-pygments
|
python-pygments
|
||||||
@ -96,6 +97,7 @@ backup=(
|
|||||||
etc/kvmd/{override,logging,auth,meta}.yaml
|
etc/kvmd/{override,logging,auth,meta}.yaml
|
||||||
etc/kvmd/{ht,ipmi,vnc}passwd
|
etc/kvmd/{ht,ipmi,vnc}passwd
|
||||||
etc/kvmd/nginx/{kvmd.ctx-{http,server},loc-{login,nocache,proxy,websocket},mime-types,ssl,nginx}.conf
|
etc/kvmd/nginx/{kvmd.ctx-{http,server},loc-{login,nocache,proxy,websocket},mime-types,ssl,nginx}.conf
|
||||||
|
etc/kvmd/janus/janus{,.plugin.ustreamer,.transport.websockets}.jcfg
|
||||||
etc/kvmd/web.css
|
etc/kvmd/web.css
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -139,6 +141,10 @@ package_kvmd() {
|
|||||||
install -Dm444 -t "$pkgdir/etc/kvmd/nginx" "$_cfg_default/nginx"/*.conf
|
install -Dm444 -t "$pkgdir/etc/kvmd/nginx" "$_cfg_default/nginx"/*.conf
|
||||||
chmod 644 "$pkgdir/etc/kvmd/nginx/nginx.conf"
|
chmod 644 "$pkgdir/etc/kvmd/nginx/nginx.conf"
|
||||||
|
|
||||||
|
mkdir -p "$pkgdir/etc/kvmd/janus"
|
||||||
|
chmod 755 "$pkgdir/etc/kvmd/janus"
|
||||||
|
install -Dm444 -t "$pkgdir/etc/kvmd/janus" "$_cfg_default/janus"/*.jcfg
|
||||||
|
|
||||||
install -Dm644 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/*.yaml
|
install -Dm644 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/*.yaml
|
||||||
install -Dm600 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/*passwd
|
install -Dm600 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/*passwd
|
||||||
install -Dm644 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/web.css
|
install -Dm644 -t "$pkgdir/etc/kvmd" "$_cfg_default/kvmd"/web.css
|
||||||
|
|||||||
19
configs/janus/janus.jcfg
Normal file
19
configs/janus/janus.jcfg
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
general: {
|
||||||
|
debug_level = 4
|
||||||
|
}
|
||||||
|
nat: {
|
||||||
|
nice_debug = false
|
||||||
|
ice_lite = true
|
||||||
|
ice_tcp = false
|
||||||
|
full_trickle = false
|
||||||
|
ignore_mdns = true
|
||||||
|
}
|
||||||
|
media: {
|
||||||
|
min_nack_queue = 2000
|
||||||
|
rtp_port_range = "20000-40000"
|
||||||
|
no_media_timer = 0
|
||||||
|
slowlink_threshold = 10
|
||||||
|
twcc_period = 100
|
||||||
|
dtls_timeout = 1000
|
||||||
|
nack_optimizations = true
|
||||||
|
}
|
||||||
3
configs/janus/janus.plugin.ustreamer.jcfg
Normal file
3
configs/janus/janus.plugin.ustreamer.jcfg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
memsink: {
|
||||||
|
object = "kvmd::ustreamer::h264"
|
||||||
|
}
|
||||||
4
configs/janus/janus.transport.websockets.jcfg
Normal file
4
configs/janus/janus.transport.websockets.jcfg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
general: {
|
||||||
|
ws = true
|
||||||
|
ws_unix = "/run/kvmd/janus-ws.sock"
|
||||||
|
}
|
||||||
19
configs/os/services/kvmd-janus-norunner.service
Normal file
19
configs/os/services/kvmd-janus-norunner.service
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Pi-KVM - Janus WebRTC Gateway (No Runner)
|
||||||
|
After=network.target network-online.target nss-lookup.target kvmd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=kvmd-janus
|
||||||
|
Group=kvmd-janus
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
AmbientCapabilities=CAP_NET_RAW
|
||||||
|
LimitNOFILE=65536
|
||||||
|
|
||||||
|
ExecStart=/usr/bin/janus --disable-colors --plugins-folder=/usr/lib/ustreamer/janus --configs-folder=/etc/kvmd/janus
|
||||||
|
TimeoutStopSec=5
|
||||||
|
KillMode=mixed
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -1,13 +1,19 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Pi-KVM - Janus WebRTC Gateway (stub)
|
Description=Pi-KVM - Janus WebRTC Gateway
|
||||||
After=network.target network-online.target nss-lookup.target kvmd.service
|
After=network.target network-online.target nss-lookup.target kvmd.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
User=kvmd-janus
|
||||||
ExecStart=/bin/true
|
Group=kvmd-janus
|
||||||
ExecStop=/bin/true
|
Type=simple
|
||||||
RemainAfterExit=true
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
AmbientCapabilities=CAP_NET_RAW
|
||||||
LimitNOFILE=65536
|
LimitNOFILE=65536
|
||||||
|
|
||||||
|
ExecStart=/usr/bin/kvmd-janus --run
|
||||||
|
TimeoutStopSec=5
|
||||||
|
KillMode=mixed
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@ -72,7 +72,7 @@ async def log_process(
|
|||||||
if stdout:
|
if stdout:
|
||||||
log = (logger.info if proc.returncode == 0 else logger.error)
|
log = (logger.info if proc.returncode == 0 else logger.error)
|
||||||
for line in stdout.split("\n"):
|
for line in stdout.split("\n"):
|
||||||
log("Console: %s", line)
|
log("=> %s", line)
|
||||||
return proc
|
return proc
|
||||||
|
|
||||||
|
|
||||||
@ -81,12 +81,34 @@ async def log_stdout_infinite(proc: asyncio.subprocess.Process, logger: logging.
|
|||||||
async for line_bytes in proc.stdout: # type: ignore
|
async for line_bytes in proc.stdout: # type: ignore
|
||||||
line = line_bytes.decode(errors="ignore").strip()
|
line = line_bytes.decode(errors="ignore").strip()
|
||||||
if line:
|
if line:
|
||||||
logger.info("Console: %s", line)
|
logger.info("=> %s", line)
|
||||||
empty = 0
|
empty = 0
|
||||||
else:
|
else:
|
||||||
empty += 1
|
empty += 1
|
||||||
if empty == 100: # asyncio bug
|
if empty == 100: # asyncio bug
|
||||||
raise RuntimeError("asyncio process: too many empty lines")
|
raise RuntimeError("Asyncio process: too many empty lines")
|
||||||
|
|
||||||
|
|
||||||
|
async def kill_process(proc: asyncio.subprocess.Process, wait: float, logger: logging.Logger) -> None: # pylint: disable=no-member
|
||||||
|
if proc.returncode is None:
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
if proc.returncode is None:
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except Exception:
|
||||||
|
if proc.returncode is not None:
|
||||||
|
raise
|
||||||
|
await proc.wait()
|
||||||
|
logger.info("Process killed: pid=%d; retcode=%d", proc.pid, proc.returncode)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
if proc.returncode is None:
|
||||||
|
logger.exception("Can't kill process pid=%d", proc.pid)
|
||||||
|
else:
|
||||||
|
logger.info("Process killed: pid=%d; retcode=%d", proc.pid, proc.returncode)
|
||||||
|
|
||||||
|
|
||||||
def rename_process(suffix: str, prefix: str="kvmd") -> None:
|
def rename_process(suffix: str, prefix: str="kvmd") -> None:
|
||||||
|
|||||||
@ -658,4 +658,29 @@ def _get_config_scheme() -> Dict:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"janus": {
|
||||||
|
"stun": {
|
||||||
|
"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"),
|
||||||
|
"timeout": Option(5.0, type=valid_float_f01, unpack_as="stun_timeout"),
|
||||||
|
},
|
||||||
|
|
||||||
|
"check": {
|
||||||
|
"interval": Option(10.0, type=valid_float_f01, unpack_as="check_interval"),
|
||||||
|
"retries": Option(5, type=valid_int_f1, unpack_as="check_retries"),
|
||||||
|
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="check_retries_delay"),
|
||||||
|
},
|
||||||
|
|
||||||
|
"cmd": Option([
|
||||||
|
"/usr/bin/janus",
|
||||||
|
"--disable-colors",
|
||||||
|
"--plugins-folder=/usr/lib/ustreamer/janus",
|
||||||
|
"--configs-folder=/etc/kvmd/janus",
|
||||||
|
"--interface={src_ip}",
|
||||||
|
"--stun-server={stun_host}:{stun_port}",
|
||||||
|
], type=valid_command),
|
||||||
|
"cmd_remove": Option([], type=valid_options),
|
||||||
|
"cmd_append": Option([], type=valid_options),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
44
kvmd/apps/janus/__init__.py
Normal file
44
kvmd/apps/janus/__init__.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# KVMD - The main Pi-KVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018-2021 Maxim Devaev <mdevaev@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
# #
|
||||||
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .. import init
|
||||||
|
|
||||||
|
from .runner import JanusRunner
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
def main(argv: Optional[List[str]]=None) -> None:
|
||||||
|
config = init(
|
||||||
|
prog="kvmd-Janus",
|
||||||
|
description="Janus WebRTC Gateway Runner",
|
||||||
|
check_run=True,
|
||||||
|
argv=argv,
|
||||||
|
)[2].janus
|
||||||
|
|
||||||
|
JanusRunner(
|
||||||
|
**config.stun._unpack(),
|
||||||
|
**config.check._unpack(),
|
||||||
|
**config._unpack(ignore=["stun", "check"]),
|
||||||
|
).run()
|
||||||
24
kvmd/apps/janus/__main__.py
Normal file
24
kvmd/apps/janus/__main__.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# KVMD - The main Pi-KVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018-2021 Maxim Devaev <mdevaev@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
# #
|
||||||
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
from . import main
|
||||||
|
main()
|
||||||
189
kvmd/apps/janus/runner.py
Normal file
189
kvmd/apps/janus/runner.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import asyncio
|
||||||
|
import asyncio.subprocess
|
||||||
|
import socket
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import netifaces
|
||||||
|
|
||||||
|
from ... import tools
|
||||||
|
from ... import aiotools
|
||||||
|
from ... import aioproc
|
||||||
|
|
||||||
|
from ...logging import get_logger
|
||||||
|
|
||||||
|
from .stun import stun_get_info
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class _Netcfg:
|
||||||
|
nat_type: str = dataclasses.field(default="")
|
||||||
|
src_ip: str = dataclasses.field(default="")
|
||||||
|
ext_ip: str = dataclasses.field(default="")
|
||||||
|
stun_host: str = dataclasses.field(default="")
|
||||||
|
stun_port: int = dataclasses.field(default=0)
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
class JanusRunner:
|
||||||
|
def __init__( # pylint: disable=too-many-instance-attributes
|
||||||
|
self,
|
||||||
|
stun_host: str,
|
||||||
|
stun_port: int,
|
||||||
|
stun_timeout: float,
|
||||||
|
|
||||||
|
check_interval: int,
|
||||||
|
check_retries: int,
|
||||||
|
check_retries_delay: float,
|
||||||
|
|
||||||
|
cmd: List[str],
|
||||||
|
cmd_remove: List[str],
|
||||||
|
cmd_append: List[str],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self.__stun_host = stun_host
|
||||||
|
self.__stun_port = stun_port
|
||||||
|
self.__stun_timeout = stun_timeout
|
||||||
|
|
||||||
|
self.__check_interval = check_interval
|
||||||
|
self.__check_retries = check_retries
|
||||||
|
self.__check_retries_delay = check_retries_delay
|
||||||
|
|
||||||
|
self.__cmd = tools.build_cmd(cmd, cmd_remove, cmd_append)
|
||||||
|
|
||||||
|
self.__janus_task: Optional[asyncio.Task] = None
|
||||||
|
self.__janus_proc: Optional[asyncio.subprocess.Process] = None # pylint: disable=no-member
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
logger = get_logger(0)
|
||||||
|
logger.info("Starting Janus Runner ...")
|
||||||
|
try:
|
||||||
|
asyncio.run(self.__run())
|
||||||
|
except (SystemExit, KeyboardInterrupt):
|
||||||
|
pass
|
||||||
|
logger.info("Bye-bye")
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def __run(self) -> None:
|
||||||
|
logger = get_logger(0)
|
||||||
|
try:
|
||||||
|
prev_netcfg: Optional[_Netcfg] = None
|
||||||
|
while True:
|
||||||
|
netcfg = _Netcfg()
|
||||||
|
for _ in range(self.__check_retries - 1):
|
||||||
|
netcfg = await self.__get_netcfg()
|
||||||
|
if netcfg.ext_ip:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(self.__check_retries_delay)
|
||||||
|
|
||||||
|
if netcfg != prev_netcfg:
|
||||||
|
logger.info("Got new %s", netcfg)
|
||||||
|
if netcfg.src_ip and netcfg.ext_ip:
|
||||||
|
logger.info("Okay, restarting Janus ...")
|
||||||
|
await self.__stop_janus()
|
||||||
|
await self.__start_janus(netcfg)
|
||||||
|
else:
|
||||||
|
logger.error("Empty src_ip or ext_ip; stopping Janus ...")
|
||||||
|
await self.__stop_janus()
|
||||||
|
prev_netcfg = netcfg
|
||||||
|
|
||||||
|
await asyncio.sleep(self.__check_interval)
|
||||||
|
except: # noqa: E722
|
||||||
|
await self.__stop_janus()
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def __get_netcfg(self) -> _Netcfg:
|
||||||
|
src_ip = (self.__get_default_ip() or "0.0.0.0")
|
||||||
|
(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)
|
||||||
|
|
||||||
|
def __get_default_ip(self) -> str:
|
||||||
|
try:
|
||||||
|
gws = netifaces.gateways()
|
||||||
|
if "default" not in gws:
|
||||||
|
raise RuntimeError(f"No default gateway: {gws}")
|
||||||
|
|
||||||
|
iface = ""
|
||||||
|
for proto in [socket.AF_INET, socket.AF_INET6]:
|
||||||
|
if proto in gws["default"]:
|
||||||
|
iface = gws["default"][proto][1]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"No iface for the gateway {gws['default']}")
|
||||||
|
|
||||||
|
for addr in netifaces.ifaddresses(iface).get(proto, []):
|
||||||
|
return addr["addr"]
|
||||||
|
except Exception as err:
|
||||||
|
get_logger().error("Can't get default IP: %s", tools.efmt(err))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def __get_stun_info(self, src_ip: str) -> Tuple[str, str]:
|
||||||
|
try:
|
||||||
|
return (await stun_get_info(
|
||||||
|
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:
|
||||||
|
get_logger().error("Can't get STUN info: %s", tools.efmt(err))
|
||||||
|
return ("", "")
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def __start_janus(self, netcfg: _Netcfg) -> None:
|
||||||
|
assert not self.__janus_task
|
||||||
|
self.__janus_task = asyncio.create_task(self.__janus_task_loop(netcfg))
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def __stop_janus(self) -> None:
|
||||||
|
if self.__janus_task:
|
||||||
|
self.__janus_task.cancel()
|
||||||
|
await asyncio.gather(self.__janus_task, return_exceptions=True)
|
||||||
|
await self.__kill_janus_proc()
|
||||||
|
self.__janus_task = None
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def __janus_task_loop(self, netcfg: _Netcfg) -> None: # pylint: disable=too-many-branches
|
||||||
|
logger = get_logger(0)
|
||||||
|
while True: # pylint: disable=too-many-nested-blocks
|
||||||
|
try:
|
||||||
|
await self.__start_janus_proc(netcfg)
|
||||||
|
assert self.__janus_proc is not None
|
||||||
|
await aioproc.log_stdout_infinite(self.__janus_proc, logger)
|
||||||
|
raise RuntimeError("Janus unexpectedly died")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
if self.__janus_proc:
|
||||||
|
logger.exception("Unexpected Janus error: pid=%d", self.__janus_proc.pid)
|
||||||
|
else:
|
||||||
|
logger.exception("Can't start Janus")
|
||||||
|
await self.__kill_janus_proc()
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def __start_janus_proc(self, netcfg: _Netcfg) -> None:
|
||||||
|
assert self.__janus_proc is None
|
||||||
|
placeholders = {
|
||||||
|
key: str(value)
|
||||||
|
for (key, value) in dataclasses.asdict(netcfg).items()
|
||||||
|
}
|
||||||
|
cmd = [
|
||||||
|
part.format(**placeholders)
|
||||||
|
for part in self.__cmd
|
||||||
|
]
|
||||||
|
self.__janus_proc = await aioproc.run_process(cmd)
|
||||||
|
get_logger(0).info("Started Janus pid=%d: %s", self.__janus_proc.pid, cmd)
|
||||||
|
|
||||||
|
async def __kill_janus_proc(self) -> None:
|
||||||
|
if self.__janus_proc:
|
||||||
|
await aioproc.kill_process(self.__janus_proc, 5, get_logger(0))
|
||||||
|
self.__janus_proc = None
|
||||||
183
kvmd/apps/janus/stun.py
Normal file
183
kvmd/apps/janus/stun.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import secrets
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ... import tools
|
||||||
|
from ... import aiotools
|
||||||
|
|
||||||
|
from ...logging import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class StunAddress:
|
||||||
|
ip: str
|
||||||
|
port: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class StunResponse:
|
||||||
|
ok: bool
|
||||||
|
ext: Optional[StunAddress] = dataclasses.field(default=None)
|
||||||
|
src: Optional[StunAddress] = dataclasses.field(default=None)
|
||||||
|
changed: Optional[StunAddress] = dataclasses.field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class StunNatType:
|
||||||
|
BLOCKED = "Blocked"
|
||||||
|
OPEN_INTERNET = "Open Internet"
|
||||||
|
SYMMETRIC_UDP_FW = "Symmetric UDP Firewall"
|
||||||
|
FULL_CONE_NAT = "Full Cone NAT"
|
||||||
|
RESTRICTED_NAT = "Restricted NAT"
|
||||||
|
RESTRICTED_PORT_NAT = "Restricted Port NAT"
|
||||||
|
SYMMETRIC_NAT = "Symmetric NAT"
|
||||||
|
CHANGED_ADDR_ERROR = "Error when testing on Changed-IP and Port"
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
async def stun_get_info(
|
||||||
|
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
|
||||||
|
|
||||||
|
(family, _, _, _, addr) = socket.getaddrinfo(src_ip, src_port, type=socket.SOCK_DGRAM)[0]
|
||||||
|
with socket.socket(family, socket.SOCK_DGRAM) as sock:
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
sock.bind(addr)
|
||||||
|
(nat_type, response) = _get_nat_type(
|
||||||
|
stun_host=stun_host,
|
||||||
|
stun_port=stun_port,
|
||||||
|
src_ip=src_ip,
|
||||||
|
sock=sock,
|
||||||
|
)
|
||||||
|
return (nat_type, (response.ext.ip if response.ext is not None else ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nat_type( # pylint: disable=too-many-return-statements
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
response = _stun_request("Change request [ext_ip == src_ip]", stun_host, stun_port, request, sock)
|
||||||
|
|
||||||
|
if first.ext.ip == src_ip:
|
||||||
|
if response.ok:
|
||||||
|
return (StunNatType.OPEN_INTERNET, response)
|
||||||
|
return (StunNatType.SYMMETRIC_UDP_FW, response)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
return (StunNatType.FULL_CONE_NAT, response)
|
||||||
|
|
||||||
|
if first.changed is None:
|
||||||
|
raise RuntimeError(f"Changed addr is None: {first}")
|
||||||
|
response = _stun_request("Change request [ext_ip != src_ip]", first.changed.ip, first.changed.port, b"", sock)
|
||||||
|
if not response.ok:
|
||||||
|
return (StunNatType.CHANGED_ADDR_ERROR, response)
|
||||||
|
|
||||||
|
if response.ext == first.ext:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def _stun_request( # pylint: disable=too-many-locals
|
||||||
|
ctx: str,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
request: bytes,
|
||||||
|
sock: socket.socket,
|
||||||
|
) -> 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
|
||||||
|
|
||||||
|
trans_id = secrets.token_bytes(16)
|
||||||
|
request = struct.pack(">HH", 0x0001, len(request)) + trans_id + request # Bind Request
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.sendto(request, (host, port))
|
||||||
|
except Exception as err:
|
||||||
|
get_logger().error("%s: Can't send request: %s", ctx, tools.efmt(err))
|
||||||
|
return StunResponse(ok=False)
|
||||||
|
try:
|
||||||
|
response = sock.recvfrom(2048)[0]
|
||||||
|
except Exception as 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])
|
||||||
|
if response_type != 0x0101:
|
||||||
|
get_logger().error("%s: Invalid response type: %#.4x", ctx, response_type)
|
||||||
|
return StunResponse(ok=False)
|
||||||
|
if trans_id != response[4:20]:
|
||||||
|
get_logger().error("%s: Transaction ID mismatch")
|
||||||
|
return StunResponse(ok=False)
|
||||||
|
|
||||||
|
parsed: Dict[str, StunAddress] = {}
|
||||||
|
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(data: bytes) -> StunAddress:
|
||||||
|
family = data[1]
|
||||||
|
if family == 1:
|
||||||
|
parts = struct.unpack(">HBBBB", data[2:8])
|
||||||
|
return StunAddress(
|
||||||
|
ip=".".join(map(str, parts[1:])),
|
||||||
|
port=parts[0],
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"Only IPv4 supported; received: {family}")
|
||||||
@ -435,26 +435,6 @@ class Streamer: # pylint: disable=too-many-instance-attributes
|
|||||||
get_logger(0).info("Started streamer pid=%d: %s", self.__streamer_proc.pid, cmd)
|
get_logger(0).info("Started streamer pid=%d: %s", self.__streamer_proc.pid, cmd)
|
||||||
|
|
||||||
async def __kill_streamer_proc(self) -> None:
|
async def __kill_streamer_proc(self) -> None:
|
||||||
logger = get_logger(0)
|
if self.__streamer_proc:
|
||||||
if self.__streamer_proc and self.__streamer_proc.returncode is None:
|
await aioproc.kill_process(self.__streamer_proc, 1, get_logger(0))
|
||||||
try:
|
|
||||||
self.__streamer_proc.terminate()
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
if self.__streamer_proc.returncode is None:
|
|
||||||
try:
|
|
||||||
self.__streamer_proc.kill()
|
|
||||||
except Exception:
|
|
||||||
if self.__streamer_proc.returncode is not None:
|
|
||||||
raise
|
|
||||||
await self.__streamer_proc.wait()
|
|
||||||
logger.info("Streamer killed: pid=%d; retcode=%d",
|
|
||||||
self.__streamer_proc.pid, self.__streamer_proc.returncode)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
if self.__streamer_proc.returncode is None:
|
|
||||||
logger.exception("Can't kill streamer pid=%d", self.__streamer_proc.pid)
|
|
||||||
else:
|
|
||||||
logger.info("Streamer killed: pid=%d; retcode=%d",
|
|
||||||
self.__streamer_proc.pid, self.__streamer_proc.returncode)
|
|
||||||
self.__streamer_proc = None
|
self.__streamer_proc = None
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -105,6 +105,7 @@ def main() -> None:
|
|||||||
"kvmd.apps.ipmi",
|
"kvmd.apps.ipmi",
|
||||||
"kvmd.apps.vnc",
|
"kvmd.apps.vnc",
|
||||||
"kvmd.apps.vnc.rfb",
|
"kvmd.apps.vnc.rfb",
|
||||||
|
"kvmd.apps.janus",
|
||||||
"kvmd.helpers",
|
"kvmd.helpers",
|
||||||
"kvmd.helpers.otgmsd",
|
"kvmd.helpers.otgmsd",
|
||||||
"kvmd.helpers.otgmsd.unlock",
|
"kvmd.helpers.otgmsd.unlock",
|
||||||
@ -131,6 +132,7 @@ def main() -> None:
|
|||||||
"kvmd-cleanup = kvmd.apps.cleanup:main",
|
"kvmd-cleanup = kvmd.apps.cleanup:main",
|
||||||
"kvmd-ipmi = kvmd.apps.ipmi:main",
|
"kvmd-ipmi = kvmd.apps.ipmi:main",
|
||||||
"kvmd-vnc = kvmd.apps.vnc:main",
|
"kvmd-vnc = kvmd.apps.vnc:main",
|
||||||
|
"kvmd-janus = kvmd.apps.janus:main",
|
||||||
"kvmd-helper-otgmsd-unlock = kvmd.helpers.otgmsd.unlock:main",
|
"kvmd-helper-otgmsd-unlock = kvmd.helpers.otgmsd.unlock:main",
|
||||||
"kvmd-helper-otgmsd-remount = kvmd.helpers.otgmsd.remount:main",
|
"kvmd-helper-otgmsd-remount = kvmd.helpers.otgmsd.remount:main",
|
||||||
],
|
],
|
||||||
|
|||||||
@ -27,6 +27,7 @@ RUN pacman -Syu --noconfirm \
|
|||||||
python-pyserial \
|
python-pyserial \
|
||||||
python-setproctitle \
|
python-setproctitle \
|
||||||
python-psutil \
|
python-psutil \
|
||||||
|
python-netifaces \
|
||||||
python-systemd \
|
python-systemd \
|
||||||
python-dbus \
|
python-dbus \
|
||||||
python-pygments \
|
python-pygments \
|
||||||
|
|||||||
@ -110,3 +110,7 @@ otgnet:
|
|||||||
- "/bin/true"
|
- "/bin/true"
|
||||||
pre_stop_cmd:
|
pre_stop_cmd:
|
||||||
- "/bin/true"
|
- "/bin/true"
|
||||||
|
|
||||||
|
janus:
|
||||||
|
cmd:
|
||||||
|
- "/bin/true"
|
||||||
|
|||||||
@ -47,3 +47,7 @@ otgnet:
|
|||||||
- "/bin/true"
|
- "/bin/true"
|
||||||
pre_stop_cmd:
|
pre_stop_cmd:
|
||||||
- "/bin/true"
|
- "/bin/true"
|
||||||
|
|
||||||
|
janus:
|
||||||
|
cmd:
|
||||||
|
- "/bin/true"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user