release 1.5.0: one panel in all channels; Apple apps (iOS, new macOS tab) only in beta/dev

- stable gets the beta panel and server features (new design, presets, firewall,
  monthly traffic, router failover over all ports/addresses + TCP, altip, HR tab,
  Windows tab, update banner); ios/ macos/ windows/ sources stay out of stable
- menu item "Update to N" under Settings when a new version is out
- mobile: top menu no longer stretches to 200px
- urllib User-Agent: Cloudflare in front of git.bobyrev.com returns 403 to Python-urllib
- docs: GitHub adleibzon-rgb as source, password/uninstall/devices sections,
  new screenshots, PORTS.md TCP fallback and second IP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrei Leibzon
2026-11-11 18:12:13 +03:00
parent 22537b3aad
commit 1f3eced228
104 changed files with 2499 additions and 8256 deletions

54
server/phobos-v6relay.py Normal file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""IPv6 -> local IPv4 UDP relay for the obfuscator (it speaks IPv4 only).
Usage: phobos-v6relay.py LISTEN_PORT [TARGET_PORT] [TARGET_HOST]
One upstream socket per IPv6 client, so the obfuscator sees a stable
client address for the whole session. Idle clients are dropped after 300 s.
"""
import selectors
import socket
import sys
import time
IDLE = 300
listen_port = int(sys.argv[1])
target = (sys.argv[3] if len(sys.argv) > 3 else "127.0.0.1",
int(sys.argv[2]) if len(sys.argv) > 2 else listen_port)
down = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
down.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
down.bind(("::", listen_port))
sel = selectors.DefaultSelector()
sel.register(down, selectors.EVENT_READ, None)
clients = {} # v6 addr -> [upstream socket, last seen]
while True:
for key, _ in sel.select(timeout=30):
if key.data is None:
data, addr = down.recvfrom(65535)
c = clients.get(addr)
if c is None:
up = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
up.connect(target)
sel.register(up, selectors.EVENT_READ, addr)
c = clients[addr] = [up, 0]
c[1] = time.monotonic()
try:
c[0].send(data)
except OSError:
pass
else:
addr = key.data
try:
down.sendto(key.fileobj.recv(65535), addr)
clients[addr][1] = time.monotonic()
except OSError:
pass
now = time.monotonic()
for addr, (up, seen) in list(clients.items()):
if now - seen > IDLE:
sel.unregister(up)
up.close()
del clients[addr]