Replace patchelf crack with Freeloader LD_PRELOAD approach

- Multi-stage Dockerfile: discover patterns from PMS binary (capstone),
  compile .so with zig (musl), layer onto lscr.io/linuxserver/plex
- Uses LD_PRELOAD instead of patchelf (which corrupts Plex's musl loader)
- Auto-discovery: broad structural patterns with string-anchored fallback
  (//feature) and relationship-based fallback (BITSET_REF within BS_INIT)
- hook.cpp uses __has_include for generated patterns with hardcoded fallbacks
- Custom wrapper.sh (no traffic_logger preload)
- Vendored Freeloader source (github.com/authrequest/Freeloader, AGPL-3.0)
- Removed stale plexmediaserver_crack.so binary
- Supports Plex 1.43.3+ (verified against 1.43.2 and 1.43.3)
This commit is contained in:
benjamin committed 2026-08-19 22:33:42 +02:00
1 parent 4399a8288d
commit 72f4661bdc
72 files changed
+77927 -17

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import os
import stat
import pytest
from plex_relay.cache import HostKeyCache, parse_known_hosts
from plex_relay.errors import HostKeyCacheError
from plex_relay.models import HostKey
def _entries():
return {
"[1.2.3.4]:443": HostKey("[1.2.3.4]:443", "ssh-ed25519", "AAAAfirst"),
"[5.6.7.8]:443": HostKey("[5.6.7.8]:443", "ssh-ed25519", "AAAAsecond"),
}
def test_save_load_roundtrip(tmp_path):
cache = HostKeyCache(tmp_path / "relayHostKey.txt")
cache.save(_entries())
loaded = HostKeyCache(tmp_path / "relayHostKey.txt").load()
assert loaded == _entries()
def test_save_is_sorted_and_blocked(tmp_path):
p = tmp_path / "relayHostKey.txt"
HostKeyCache(p).save(_entries())
text = p.read_text()
assert text.index("[1.2.3.4]") < text.index("[5.6.7.8]")
assert "# [1.2.3.4]:443\n[1.2.3.4]:443 ssh-ed25519 AAAAfirst\n" in text
@pytest.mark.skipif(os.name != "posix", reason="POSIX file modes only")
def test_save_is_0600(tmp_path):
p = tmp_path / "relayHostKey.txt"
HostKeyCache(p).save(_entries())
assert stat.S_IMODE(os.stat(p).st_mode) == 0o600
def test_missing_file_loads_empty(tmp_path):
assert HostKeyCache(tmp_path / "nope.txt").load() == {}
def test_malformed_file_is_rebuilt_empty(tmp_path):
p = tmp_path / "relayHostKey.txt"
p.write_text("not a comment\ngarbage line\n")
assert HostKeyCache(p).load() == {}
assert p.read_text() == ""
@pytest.mark.parametrize("lines", [
["data without marker"],
["# marker only"],
["# marker", "too many tokens here now"],
])
def test_parse_rejects_malformed(lines):
with pytest.raises(HostKeyCacheError):
parse_known_hosts(lines)
@@ -0,0 +1,44 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import dataclasses
import pytest
from plex_relay.config import RelayConfig
from plex_relay.errors import ConfigError
def test_valid_config_and_cache_path(tmp_path):
cfg = RelayConfig(token="t", ssh_user="u", data_dir=tmp_path)
assert cfg.cache_path == tmp_path / "relayHostKey.txt"
assert cfg.gating_ok() is True
@pytest.mark.parametrize("kwargs", [
{"token": "", "ssh_user": "u"},
{"token": "t", "ssh_user": ""},
{"token": "t", "ssh_user": "u", "local_port": 0},
{"token": "t", "ssh_user": "u", "local_port": 70000},
{"token": "t", "ssh_user": "u", "key_ttl_seconds": 0},
{"token": "t", "ssh_user": "u", "reap_interval_seconds": -1},
])
def test_invalid_config_raises(kwargs):
with pytest.raises(ConfigError):
RelayConfig(**kwargs)
def test_gating_requires_all_three():
base = dict(token="t", ssh_user="u")
assert RelayConfig(**base, relay_enabled=False).gating_ok() is False
assert RelayConfig(**base, published=False).gating_ok() is False
assert RelayConfig(**base, signed_in=False).gating_ok() is False
def test_token_is_not_in_repr():
cfg = RelayConfig(token="SUPERSECRET", ssh_user="u")
assert "SUPERSECRET" not in repr(cfg)
def test_config_is_frozen():
cfg = RelayConfig(token="t", ssh_user="u")
with pytest.raises(dataclasses.FrozenInstanceError):
cfg.local_port = 1 # type: ignore[misc]
@@ -0,0 +1,157 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from pathlib import Path
import pytest
from plex_relay.config import RelayConfig
from plex_relay.controller import RelayController
from plex_relay.errors import RelayError, TunnelError
from plex_relay.models import HostKey, RelayKey, known_hosts_endpoint
class FakeTunnel:
def __init__(self, host, fail=False):
self._host = host
self._fail = fail
self.alive = False
self.stopped = False
@property
def host(self):
return self._host
def start(self):
if self._fail:
raise TunnelError("spawn failed")
self.alive = True
def is_alive(self):
return self.alive
def stop(self, timeout=None):
self.stopped = True
self.alive = False
class FakeFactory:
def __init__(self):
self.fail = False
self.created = []
def __call__(self, host, port, known_hosts_path):
t = FakeTunnel(host, fail=self.fail)
self.created.append(t)
return t
class FakeTrust:
def __init__(self):
self.calls = []
def ensure_trusted(self, host, port=443, *, force=False):
self.calls.append((host, port))
return HostKey.for_endpoint(known_hosts_endpoint(host, port), RelayKey("ssh-ed25519", "k"))
@property
def known_hosts_path(self):
return Path("/k")
def build(**cfgkw):
cfg = RelayConfig(token="t", ssh_user="u", reap_interval_seconds=999.0, **cfgkw)
trust, factory = FakeTrust(), FakeFactory()
return RelayController(cfg, trust, factory), trust, factory
def test_connect_tracks_and_trusts():
ctrl, trust, factory = build()
try:
assert ctrl.connect("relay.example", 443) is True
assert ctrl.active_hosts == ["relay.example"]
assert trust.calls == [("relay.example", 443)]
assert len(factory.created) == 1
finally:
ctrl.stop()
def test_connect_dedup_when_alive():
ctrl, _, factory = build()
try:
assert ctrl.connect("h") is True
assert ctrl.connect("h") is False
assert len(factory.created) == 1
finally:
ctrl.stop()
def test_reconnect_after_death():
ctrl, _, factory = build()
try:
ctrl.connect("h")
factory.created[0].alive = False # tunnel died
assert ctrl.connect("h") is True
assert len(factory.created) == 2
finally:
ctrl.stop()
def test_connect_raises_on_tunnel_failure():
ctrl, _, factory = build()
factory.fail = True
with pytest.raises(RelayError):
ctrl.connect("h")
assert ctrl.active_hosts == []
ctrl.stop()
def test_start_relay_blocked_by_gating():
for kw in ({"relay_enabled": False}, {"published": False}, {"signed_in": False}):
ctrl, trust, _ = build(**kw)
assert ctrl.start_relay("h") is False
assert trust.calls == []
ctrl.stop()
def test_start_relay_is_resilient_to_failure():
ctrl, _, factory = build()
factory.fail = True
assert ctrl.start_relay("h") is False # logs + swallows, does not raise
ctrl.stop()
def test_start_relay_ok():
ctrl, _, _ = build()
try:
assert ctrl.start_relay("h", 443) is True
assert ctrl.active_hosts == ["h"]
finally:
ctrl.stop()
def test_reaper_removes_dead():
ctrl, _, factory = build()
try:
ctrl.connect("a")
ctrl.connect("b")
factory.created[0].alive = False
assert ctrl.reap_once() == ["a"]
assert ctrl.active_hosts == ["b"]
assert factory.created[0].stopped is True
finally:
ctrl.stop()
def test_stop_terminates_all_and_closes():
ctrl, _, factory = build()
ctrl.connect("a")
ctrl.connect("b")
ctrl.stop()
assert ctrl.active_hosts == []
assert all(t.stopped for t in factory.created)
with pytest.raises(RelayError):
ctrl.connect("c")
def test_from_config_builds_controller(tmp_path):
cfg = RelayConfig(token="t", ssh_user="u", data_dir=tmp_path)
assert isinstance(RelayController.from_config(cfg), RelayController)
+70
View File
@@ -0,0 +1,70 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import pytest
from plex_relay.errors import RelayKeyError
from plex_relay.keys import HttpsRelayKeyFetcher, RelayKeyProvider
from plex_relay.models import RelayKey
PUB = "ssh-ed25519 AAAAkeydata comment"
KEY = RelayKey("ssh-ed25519", "AAAAkeydata")
def test_provider_respects_ttl_and_force():
calls = []
now = [1000.0]
provider = RelayKeyProvider(
"https://x/relay_v1.pub", 86_400.0,
fetcher=lambda url: (calls.append(url), PUB)[1],
clock=lambda: now[0],
)
assert provider.get() == KEY
now[0] += 3600 # within TTL -> reuse
provider.get()
assert len(calls) == 1
now[0] += 86_400 # past TTL -> refetch
provider.get()
assert len(calls) == 2
provider.get(force=True) # force -> refetch
assert len(calls) == 3
# --- HttpsRelayKeyFetcher ---------------------------------------------------
class _FakeResp:
def __init__(self, data: bytes):
self._data = data
def read(self, n: int = -1) -> bytes:
return self._data[:n] if n >= 0 else self._data
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def test_fetcher_rejects_non_https():
f = HttpsRelayKeyFetcher(opener=lambda *a, **k: _FakeResp(b""))
with pytest.raises(RelayKeyError):
f("http://insecure/relay_v1.pub")
def test_fetcher_allows_insecure_when_opted_in():
f = HttpsRelayKeyFetcher(allow_insecure=True, opener=lambda *a, **k: _FakeResp(PUB.encode()))
assert f("file:///tmp/relay_v1.pub") == PUB
def test_fetcher_caps_response_size():
big = b"x" * 100
f = HttpsRelayKeyFetcher(max_bytes=10, opener=lambda *a, **k: _FakeResp(big))
with pytest.raises(RelayKeyError, match="exceeds"):
f("https://x/relay_v1.pub")
def test_fetcher_wraps_transport_errors():
def boom(*a, **k):
raise OSError("connection refused")
f = HttpsRelayKeyFetcher(opener=boom)
with pytest.raises(RelayKeyError, match="failed to fetch"):
f("https://x/relay_v1.pub")
@@ -0,0 +1,45 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import pytest
from plex_relay.errors import RelayKeyError
from plex_relay.models import HostKey, RelayKey, known_hosts_endpoint, parse_relay_pub
PUB = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIabc relay@plex" # keytype keydata comment
KH = "* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIabc" # host keytype keydata
EXPECT = RelayKey("ssh-ed25519", "AAAAC3NzaC1lZDI1NTE5AAAAIabc")
def test_parse_pubkey_form():
assert parse_relay_pub(PUB) == EXPECT
def test_parse_known_hosts_form():
assert parse_relay_pub(KH) == EXPECT
def test_parse_skips_comments_and_blanks():
assert parse_relay_pub(f"# header\n\n{PUB}\n") == EXPECT
@pytest.mark.parametrize("bad", ["ssh-ed25519 onlytwo", "aaa bbb ccc", "", "# only comment\n"])
def test_parse_rejects_bad_payloads(bad):
with pytest.raises(RelayKeyError):
parse_relay_pub(bad)
def test_endpoint_bracket_notation():
assert known_hosts_endpoint("1.2.3.4", 443) == "[1.2.3.4]:443"
assert known_hosts_endpoint("relay.example", 2222) == "[relay.example]:2222"
assert known_hosts_endpoint("relay.example", 22) == "relay.example"
def test_endpoint_rejects_empty_host():
with pytest.raises(RelayKeyError):
known_hosts_endpoint(" ", 443)
def test_hostkey_serialization():
hk = HostKey.for_endpoint("[1.2.3.4]:443", EXPECT)
assert hk.known_hosts_line() == "[1.2.3.4]:443 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIabc"
assert hk.cache_block() == f"# [1.2.3.4]:443\n{hk.known_hosts_line()}\n"
assert hk.key == EXPECT
+61
View File
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from pathlib import Path
from plex_relay.keys import RelayKeyProvider
from plex_relay.store import HostKeyManager
class FakeCache:
def __init__(self):
self.entries = {}
self.saves = 0
@property
def path(self) -> Path:
return Path("/tmp/relayHostKey.txt")
def load(self):
return dict(self.entries)
def save(self, entries):
self.saves += 1
self.entries = dict(entries)
def _provider(text_box):
return RelayKeyProvider("https://x/relay_v1.pub", 86_400.0,
fetcher=lambda u: text_box[0], clock=lambda: 0.0)
def test_ensure_trusted_pins_and_persists():
cache = FakeCache()
mgr = HostKeyManager(_provider(["ssh-ed25519 AAAAfirst c"]), cache)
entry = mgr.ensure_trusted("1.2.3.4", 443)
assert entry.known_hosts_line() == "[1.2.3.4]:443 ssh-ed25519 AAAAfirst"
assert cache.saves == 1
assert "[1.2.3.4]:443" in cache.entries
def test_ensure_trusted_is_idempotent():
cache = FakeCache()
mgr = HostKeyManager(_provider(["ssh-ed25519 AAAAfirst c"]), cache)
mgr.ensure_trusted("1.2.3.4", 443)
mgr.ensure_trusted("1.2.3.4", 443) # unchanged -> no extra write
assert cache.saves == 1
def test_ensure_trusted_rewrites_on_key_change():
cache = FakeCache()
box = ["ssh-ed25519 AAAAfirst c"]
mgr = HostKeyManager(_provider(box), cache)
mgr.ensure_trusted("h", 443)
box[0] = "ssh-ed25519 AAAAsecond c"
entry = mgr.ensure_trusted("h", 443, force=True)
assert entry.keydata == "AAAAsecond"
assert cache.saves == 2
def test_known_hosts_path_is_cache_path():
cache = FakeCache()
mgr = HostKeyManager(_provider(["ssh-ed25519 k c"]), cache)
assert mgr.known_hosts_path == cache.path
@@ -0,0 +1,91 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from pathlib import Path
import pytest
from plex_relay.config import RelayConfig
from plex_relay.errors import TunnelError
from plex_relay.tunnel import SubprocessTunnel, SubprocessTunnelFactory, build_ssh_argv
def cfg(**kw):
base = dict(token="secret", ssh_user="machineid", local_host="127.0.0.1", local_port=32400)
base.update(kw)
return RelayConfig(**base)
def test_argv_matches_binary_layout():
argv = build_ssh_argv(cfg(), "relay.example", 443, Path("/data/relayHostKey.txt"))
assert argv == [
"ssh", "-p", "443", "-N", "-R", "0:127.0.0.1:32400",
"-o", "UserKnownHostsFile=/data/relayHostKey.txt",
"-o", "LogLevel=VERBOSE",
"-o", "PreferredAuthentications=password",
"-o", "PubkeyAuthentication=no",
"-l", "machineid", "-F", "/dev/null", "relay.example",
]
def test_argv_honours_port_and_target():
argv = build_ssh_argv(cfg(local_host="10.0.0.5", local_port=32500), "h", 2222, Path("/k"))
assert "0:10.0.0.5:32500" in argv
assert argv[argv.index("-p") + 1] == "2222"
class FakeProc:
def __init__(self):
self.alive = True
self.terminated = False
def poll(self):
return None if self.alive else 0
def terminate(self):
self.terminated = True
self.alive = False
def kill(self):
self.alive = False
def wait(self, timeout=None):
self.alive = False
return 0
def test_tunnel_start_sets_secret_env_and_cleans_askpass():
captured = {}
def spawner(argv, env):
captured["argv"] = argv
captured["env"] = dict(env)
return FakeProc()
t = SubprocessTunnel(cfg(), "relay.example", 443, Path("/k"), spawner)
t.start()
assert t.is_alive()
assert captured["env"]["PLEXTOKEN"] == "secret"
assert "SSH_ASKPASS" in captured["env"]
askpass = Path(captured["env"]["SSH_ASKPASS"])
assert askpass.exists()
assert "secret" not in captured["argv"] # never on the command line
t.stop()
assert not t.is_alive()
assert not askpass.exists() # helper removed on stop
def test_tunnel_start_wraps_spawn_failure():
def boom(argv, env):
raise OSError("ssh not found")
t = SubprocessTunnel(cfg(), "h", 443, Path("/k"), boom)
with pytest.raises(TunnelError):
t.start()
assert not t.is_alive()
def test_factory_builds_tunnel():
factory = SubprocessTunnelFactory(cfg(), spawner=lambda a, e: FakeProc())
t = factory("h", 443, Path("/k"))
assert t.host == "h"
t.start()
assert t.is_alive()
t.stop()