mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 23:30:45 +08:00
Linux installer: validate the listen address instead of reporting every port as busy.
An entry like "all" made every port probe fail and the installer said "port in use" forever. Aliases (all/*, local) are normalized, other values must be an IPv4 address of this host, and port-free now tells a busy port (exit 1) from a bad address (exit 3). Same check for set-bind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
72baceb8b1
commit
78db39eabe
3 changed files with 75 additions and 13 deletions
|
|
@ -156,9 +156,12 @@ pick_port() { # pick_port <label> <value> <default> <host> <allow_current>
|
||||||
[[ "${INTERACTIVE}" == "1" ]] || die "${p}"
|
[[ "${INTERACTIVE}" == "1" ]] || die "${p}"
|
||||||
err "${p}"; val=""; continue
|
err "${p}"; val=""; continue
|
||||||
fi
|
fi
|
||||||
if [[ "${p}" == "${allow}" ]] || netx_admin port-free "${p}" --host "${host}"; then
|
local rc=0
|
||||||
|
[[ "${p}" == "${allow}" ]] || netx_admin port-free "${p}" --host "${host}" || rc=$?
|
||||||
|
if [[ "${rc}" == "0" ]]; then
|
||||||
echo "${p}"; return 0
|
echo "${p}"; return 0
|
||||||
fi
|
fi
|
||||||
|
[[ "${rc}" == "3" ]] && die "$(t "监听地址无效" "invalid listen address"): ${host}"
|
||||||
local msg
|
local msg
|
||||||
msg="$(t "端口 ${p} 已被占用" "port ${p} is in use")"
|
msg="$(t "端口 ${p} 已被占用" "port ${p} is in use")"
|
||||||
[[ "${INTERACTIVE}" == "1" ]] || die "port_in_use: ${p} — ${msg}"
|
[[ "${INTERACTIVE}" == "1" ]] || die "port_in_use: ${p} — ${msg}"
|
||||||
|
|
@ -176,13 +179,23 @@ configure() {
|
||||||
|
|
||||||
echo
|
echo
|
||||||
info "$(t "配置 NetX" "Configure NetX")"
|
info "$(t "配置 NetX" "Configure NetX")"
|
||||||
if [[ -z "${BIND}" ]]; then
|
local b
|
||||||
if [[ "${INTERACTIVE}" == "1" ]]; then
|
while :; do
|
||||||
BIND="$(ask "$(t "监听地址(0.0.0.0=所有网卡,127.0.0.1=仅本机)" "Listen address (0.0.0.0 = all, 127.0.0.1 = local only)")" "${old_bind:-0.0.0.0}")"
|
if [[ -z "${BIND}" ]]; then
|
||||||
else
|
if [[ "${INTERACTIVE}" == "1" ]]; then
|
||||||
BIND="${old_bind:-0.0.0.0}"
|
BIND="$(ask "$(t "监听地址(回车或 0.0.0.0 = 所有网卡;127.0.0.1 = 仅本机;或本机某个 IP)" "Listen address (Enter/0.0.0.0 = all interfaces, 127.0.0.1 = local only, or one of this server's IPs)")" "${old_bind:-0.0.0.0}")"
|
||||||
|
else
|
||||||
|
BIND="${old_bind:-0.0.0.0}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
if b="$(netx_admin norm-bind -- "${BIND}" 2>&1)"; then
|
||||||
|
BIND="${b}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
[[ "${INTERACTIVE}" == "1" ]] || die "${b}"
|
||||||
|
err "${b}"
|
||||||
|
BIND=""
|
||||||
|
done
|
||||||
API_PORT="$(pick_port "$(t "Web/API 端口" "Web/API port")" "${API_PORT}" "${old_port:-8890}" "${BIND}" "${old_port}")"
|
API_PORT="$(pick_port "$(t "Web/API 端口" "Web/API port")" "${API_PORT}" "${old_port:-8890}" "${BIND}" "${old_port}")"
|
||||||
|
|
||||||
if [[ -z "${DB_MODE}" ]]; then
|
if [[ -z "${DB_MODE}" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -102,15 +102,50 @@ def write_env(path: Path, values: dict[str, str], unset: list[str] | None = None
|
||||||
# --------------------------------------------------------------------------- net
|
# --------------------------------------------------------------------------- net
|
||||||
|
|
||||||
|
|
||||||
|
_BIND_ALIASES = {"": "0.0.0.0", "all": "0.0.0.0", "any": "0.0.0.0", "*": "0.0.0.0",
|
||||||
|
"local": "127.0.0.1", "localhost": "127.0.0.1", "loopback": "127.0.0.1"}
|
||||||
|
|
||||||
|
|
||||||
|
def norm_bind(value: str) -> str:
|
||||||
|
"""Listen address as NetX needs it: an IPv4 literal present on this host.
|
||||||
|
|
||||||
|
Accepts aliases (all/*, local/localhost). Raises ValueError with a reason.
|
||||||
|
"""
|
||||||
|
v = _BIND_ALIASES.get(value.strip().lower(), value.strip())
|
||||||
|
try:
|
||||||
|
socket.inet_aton(v)
|
||||||
|
if v.count(".") != 3:
|
||||||
|
raise OSError
|
||||||
|
except OSError:
|
||||||
|
raise ValueError(f"not an IPv4 address: {value!r} (use 0.0.0.0 for all interfaces)")
|
||||||
|
if v != "0.0.0.0":
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
s.bind((v, 0))
|
||||||
|
except OSError:
|
||||||
|
raise ValueError(f"{v} is not an address of this server")
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
def port_free(port: int, host: str = "0.0.0.0") -> bool:
|
def port_free(port: int, host: str = "0.0.0.0") -> bool:
|
||||||
"""True when nothing listens on `port` (checks the wildcard and loopback)."""
|
"""True when nothing listens on `port` (checks the wildcard and loopback).
|
||||||
|
|
||||||
|
Only EADDRINUSE/EACCES mean "taken"; any other bind error is a bad address
|
||||||
|
and must not be reported as a busy port.
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
|
||||||
for h in {host, "0.0.0.0", "127.0.0.1"}:
|
for h in {host, "0.0.0.0", "127.0.0.1"}:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
s.bind((h, port))
|
s.bind((h, port))
|
||||||
except OSError:
|
except OSError as exc:
|
||||||
return False
|
if exc.errno in (errno.EADDRINUSE, errno.EACCES):
|
||||||
|
return False
|
||||||
|
raise ValueError(f"cannot bind {h}:{port}: {exc}")
|
||||||
finally:
|
finally:
|
||||||
s.close()
|
s.close()
|
||||||
# Also catch a listener bound only to a specific address we did not test.
|
# Also catch a listener bound only to a specific address we did not test.
|
||||||
|
|
@ -449,7 +484,19 @@ def cmd_env_export(a: argparse.Namespace) -> None:
|
||||||
|
|
||||||
|
|
||||||
def cmd_port_free(a: argparse.Namespace) -> None:
|
def cmd_port_free(a: argparse.Namespace) -> None:
|
||||||
sys.exit(0 if port_free(valid_port(a.port), a.host) else 1)
|
"""exit 0 = free, 1 = in use, 3 = bad listen address."""
|
||||||
|
try:
|
||||||
|
free = port_free(valid_port(a.port), a.host)
|
||||||
|
except ValueError as exc:
|
||||||
|
die("bad_listen_address", str(exc), rc=3)
|
||||||
|
sys.exit(0 if free else 1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_norm_bind(a: argparse.Namespace) -> None:
|
||||||
|
try:
|
||||||
|
print(norm_bind(a.value))
|
||||||
|
except ValueError as exc:
|
||||||
|
die("bad_listen_address", str(exc))
|
||||||
|
|
||||||
|
|
||||||
def cmd_valid_port(a: argparse.Namespace) -> None:
|
def cmd_valid_port(a: argparse.Namespace) -> None:
|
||||||
|
|
@ -636,6 +683,7 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
s = sub.add_parser("env-export"); s.add_argument("file"); s.set_defaults(fn=cmd_env_export)
|
s = sub.add_parser("env-export"); s.add_argument("file"); s.set_defaults(fn=cmd_env_export)
|
||||||
s = sub.add_parser("port-free"); s.add_argument("port"); s.add_argument("--host", default="0.0.0.0")
|
s = sub.add_parser("port-free"); s.add_argument("port"); s.add_argument("--host", default="0.0.0.0")
|
||||||
s.set_defaults(fn=cmd_port_free)
|
s.set_defaults(fn=cmd_port_free)
|
||||||
|
s = sub.add_parser("norm-bind"); s.add_argument("value"); s.set_defaults(fn=cmd_norm_bind)
|
||||||
s = sub.add_parser("valid-port"); s.add_argument("port"); s.set_defaults(fn=cmd_valid_port)
|
s = sub.add_parser("valid-port"); s.add_argument("port"); s.set_defaults(fn=cmd_valid_port)
|
||||||
s = sub.add_parser("gen-secret"); s.add_argument("kind", choices=["fernet", "password"]); s.set_defaults(fn=cmd_gen_secret)
|
s = sub.add_parser("gen-secret"); s.add_argument("kind", choices=["fernet", "password"]); s.set_defaults(fn=cmd_gen_secret)
|
||||||
s = sub.add_parser("build-url")
|
s = sub.add_parser("build-url")
|
||||||
|
|
|
||||||
|
|
@ -287,8 +287,9 @@ cmd_set_port() {
|
||||||
|
|
||||||
cmd_set_bind() {
|
cmd_set_bind() {
|
||||||
need_root set-bind; need_install
|
need_root set-bind; need_install
|
||||||
local h="${1:-}"
|
local h
|
||||||
[[ -n "${h}" ]] || die "usage: netxctl set-bind <0.0.0.0|127.0.0.1|ip>"
|
[[ -n "${1:-}" ]] || die "usage: netxctl set-bind <0.0.0.0|127.0.0.1|ip>"
|
||||||
|
h="$(netx_admin norm-bind -- "$1")" || exit 1
|
||||||
local pairs=("NETX_HOST=${h}")
|
local pairs=("NETX_HOST=${h}")
|
||||||
# Same prerequisites NetX enforces for a non-loopback bind (security_bootstrap.py).
|
# Same prerequisites NetX enforces for a non-loopback bind (security_bootstrap.py).
|
||||||
if ! is_loopback "${h}"; then
|
if ! is_loopback "${h}"; then
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue