psiphon 2.0.0__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psiphon
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Foreground native Psiphon packet tunnel and explicit local proxy for Linux
5
5
  Author: KpihX
6
6
  Author-email: KpihX <kapoivha@gmail.com>
@@ -69,6 +69,11 @@ the worker refuses before mutations. Cancellation starts no supervisor or route
69
69
  For agent-operated privilege, launch connect only in an armed tmux pane in the
70
70
  agent's current window. No sudo or privileged operation runs in offline checks.
71
71
 
72
+ Logs live in `$XDG_STATE_HOME/psiphon/psiphon.log` (default
73
+ `~/.local/state/psiphon/`). Before a new connection, a 2 MiB log rotates through
74
+ four numbered prior logs (`.1` to `.4`), bounding retained log storage to about
75
+ 10 MiB. The oldest retained log is discarded on the next rotation.
76
+
72
77
  **Escalation trust:** sudo approves this local Python interpreter and installed
73
78
  package, including an editable checkout if that is what you installed. They may
74
79
  be user-writable. `-I` avoids ambient PYTHONPATH/user-site, but does not make that
@@ -52,6 +52,11 @@ the worker refuses before mutations. Cancellation starts no supervisor or route
52
52
  For agent-operated privilege, launch connect only in an armed tmux pane in the
53
53
  agent's current window. No sudo or privileged operation runs in offline checks.
54
54
 
55
+ Logs live in `$XDG_STATE_HOME/psiphon/psiphon.log` (default
56
+ `~/.local/state/psiphon/`). Before a new connection, a 2 MiB log rotates through
57
+ four numbered prior logs (`.1` to `.4`), bounding retained log storage to about
58
+ 10 MiB. The oldest retained log is discarded on the next rotation.
59
+
55
60
  **Escalation trust:** sudo approves this local Python interpreter and installed
56
61
  package, including an editable checkout if that is what you installed. They may
57
62
  be user-writable. `-I` avoids ambient PYTHONPATH/user-site, but does not make that
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "psiphon"
3
- version = "2.0.0"
3
+ version = "2.0.2"
4
4
  description = "Foreground native Psiphon packet tunnel and explicit local proxy for Linux"
5
5
  readme = "README.md"
6
6
  license = "BUSL-1.1"
@@ -50,6 +50,8 @@ RUNTIME_DIR = _runtime / f"psiphon-{os.getuid()}"
50
50
  ACTIVE_CONFIG = CONFIG_DIR / "psiphon.config"
51
51
  PID_FILE = RUNTIME_DIR / "psiphon.pid"
52
52
  LOG_FILE = STATE_DIR / "psiphon.log"
53
+ LOG_MAX_BYTES = 2 * 1024 * 1024
54
+ LOG_BACKUPS = 4
53
55
  SESSION_FILE = RUNTIME_DIR / "session.json"
54
56
  DATA_DIR = CACHE_DIR / "core" # signed server-list cache and tunnel-core datastore
55
57
 
@@ -17,6 +17,7 @@ from typing import Annotated, Any
17
17
  import requests
18
18
  import typer
19
19
  from rich.console import Console
20
+ from rich.table import Table
20
21
 
21
22
  from psiphon import config, engine, network, routing
22
23
  from psiphon.supervisor import RollbackError, Supervisor
@@ -112,6 +113,23 @@ def notice_state(offset: int = 0) -> dict[str, Any]:
112
113
  return state
113
114
 
114
115
 
116
+ def rotate_log() -> None:
117
+ """Keep a bounded local history before opening a new connection log."""
118
+ if (
119
+ not config.LOG_FILE.exists()
120
+ or config.LOG_FILE.stat().st_size < config.LOG_MAX_BYTES
121
+ ):
122
+ return
123
+ oldest = config.LOG_FILE.with_name(f"{config.LOG_FILE.name}.{config.LOG_BACKUPS}")
124
+ if oldest.exists():
125
+ oldest.unlink()
126
+ for number in range(config.LOG_BACKUPS - 1, 0, -1):
127
+ source = config.LOG_FILE.with_name(f"{config.LOG_FILE.name}.{number}")
128
+ if source.exists():
129
+ source.replace(source.with_name(f"{config.LOG_FILE.name}.{number + 1}"))
130
+ config.LOG_FILE.replace(config.LOG_FILE.with_name(f"{config.LOG_FILE.name}.1"))
131
+
132
+
115
133
  def process_identity(pid: int) -> dict[str, str]:
116
134
  proc = Path(f"/proc/{pid}")
117
135
  stat = (proc / "stat").read_text().rsplit(") ", 1)[1].split()
@@ -169,7 +187,8 @@ def connected(region: str, proxy: bool, interface: str | None, wait_s: float):
169
187
  elif interface:
170
188
  uplink = interface
171
189
  engine.write_config(region, proxy)
172
- # Create a private log before the worker opens it; never truncate history.
190
+ # Create a private log before the worker opens it; preserve bounded history.
191
+ rotate_log()
173
192
  fd = os.open(
174
193
  config.LOG_FILE,
175
194
  os.O_APPEND | os.O_CREAT | os.O_WRONLY | os.O_NOFOLLOW,
@@ -341,12 +360,7 @@ def status(
341
360
  "-p",
342
361
  help="Measure explicit local proxy instead of active OS route",
343
362
  ),
344
- geo: bool = typer.Option(
345
- False,
346
- "--geo",
347
- "-g",
348
- help="Query third-party GeoIP (not physical-location proof)",
349
- ),
363
+ geo: bool = typer.Option(False, "--geo", "-g", help="Query third-party GeoIP"),
350
364
  download: bool = typer.Option(False, "--download", "-d"),
351
365
  upload: bool = typer.Option(False, "--upload", "-u"),
352
366
  speed: bool = typer.Option(
@@ -355,17 +369,61 @@ def status(
355
369
  seconds: float = typer.Option(
356
370
  config.BENCH_DEFAULT_S, "--seconds", "-n", min=1, max=60
357
371
  ),
372
+ output_format: str = typer.Option("table", "--format", "-f", help="table or json"),
358
373
  ) -> None:
359
374
  """Inspect the current route and HTTPS egress, even without Psiphon running."""
375
+ if output_format not in ("table", "json"):
376
+ raise typer.BadParameter("format must be table or json")
360
377
  route = routing.active_route()
361
- console.print("Active OS route: " + json.dumps(route), markup=False)
362
378
  transport = proxy_url(proxy)
363
379
  address = network.public_ip(transport)
364
- console.print(
365
- f"{'Explicit proxy' if proxy else 'Active route'} HTTPS IP: {address}"
366
- )
380
+ location = {}
367
381
  if geo:
368
- console.print(network.geo(address, transport))
382
+ try:
383
+ location = network.geo(address, transport)
384
+ except (ConnectionError, requests.RequestException):
385
+ location = {}
386
+ state = {}
387
+ if config.SESSION_FILE.exists():
388
+ try:
389
+ state = json.loads(config.SESSION_FILE.read_text())
390
+ except (OSError, ValueError):
391
+ state = {"state": "unreadable"}
392
+ payload: dict[str, Any] = {
393
+ "mode": "explicit-proxy" if proxy else "active-route",
394
+ "interface": route.get("dev", ""),
395
+ "local_ipv4": route.get("prefsrc", route.get("src", "")),
396
+ "gateway": route.get("gateway", ""),
397
+ "public_ipv4": address,
398
+ "location": location,
399
+ "session": state,
400
+ }
401
+ if output_format == "json":
402
+ console.print_json(json.dumps(payload))
403
+ else:
404
+ table = Table(title="Current IPv4 Network Status", show_header=False)
405
+ table.add_column("Property", style="cyan")
406
+ table.add_column("Value")
407
+ table.add_row("Mode", payload["mode"])
408
+ table.add_row("Active interface", payload["interface"] or "not reported")
409
+ table.add_row("Local IPv4", payload["local_ipv4"] or "not reported")
410
+ table.add_row("Gateway", payload["gateway"] or "point-to-point or not reported")
411
+ table.add_row("Public IPv4", address)
412
+ country = " ".join(
413
+ str(value)
414
+ for value in (location.get("country", ""), location.get("country_code", ""))
415
+ if value
416
+ )
417
+ table.add_row(
418
+ "Location (indicative)",
419
+ ", ".join(
420
+ value for value in (country, str(location.get("city", ""))) if value
421
+ )
422
+ or ("unavailable" if geo else "not requested; use -g"),
423
+ )
424
+ table.add_row("Requested region", str(state.get("region", "not connected")))
425
+ table.add_row("Tunnel mode", str(state.get("mode", "not connected")))
426
+ console.print(table)
369
427
  for enabled, name, measure in (
370
428
  (download or speed, "download", network.download),
371
429
  (upload or speed, "upload", network.upload),
@@ -373,7 +431,7 @@ def status(
373
431
  if enabled:
374
432
  result = measure(seconds, transport)
375
433
  console.print(
376
- f"{name}: {result.mbps:.3f} Mbps; {result.bytes_received} bytes / {result.elapsed_s:.3f}s"
434
+ f"[green]{name.title()}:[/green] {result.mbps:.2f} Mbps · {result.bytes_received / 1_000_000:.1f} MB in {result.elapsed_s:.1f}s"
377
435
  )
378
436
 
379
437
 
File without changes
File without changes
File without changes
File without changes
File without changes