Compare commits

..

No commits in common. "c3b50460cd0dd87067fdd9163d561b09facc2479" and "9d0d14046790d48ce4a5b320ad54ac2593dafed1" have entirely different histories.

4 changed files with 36 additions and 177 deletions

View file

@ -19,7 +19,6 @@ class ResultError(Enum):
SUBSCRIPTION = "subscription"
PROCESS_GOT_KILLED = "process_got_killed"
PROCESS_WONT_START = "process_wont_start"
PROCESS_MISMATCH = "process_mismatch"
NMCLI = "nmcli_issues"
FIREWALL = "firewall"
CLIENT_DNS = "client_dns"
@ -27,7 +26,6 @@ class ResultError(Enum):
INTERFACE = "interface"
TIMEOUT = "timeout"
INVALID_API_REPLY = "invalid_api_reply"
LEAK_ISSUE = "leak_issue"
UNKNOWN = "unknown"
@dataclass

View file

@ -1,96 +0,0 @@
from core.services.networking.httpx.classify_response import classify_response
# models
from core.models.Result import Result, ResultError
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# errors/loggers/observers
from core.Constants import Constants
from core.errors.logger import logger
from essentials.observers.ConnectionObserver import ConnectionObserver
# generic
import time
import random
import httpx
from typing import Optional
API_ENDPOINT_FORMATS = {
"https://api.ipify.org?format=json": "ip",
"https://api.myip.com": "ip",
"https://wtfismyip.com/json": "YourFuckingIPAddress",
"https://httpbin.org/ip": "origin",
"https://api.myip.com": "ip",
"https://ipinfo.io/json": "ip"
}
MAX_ATTEMPTS = 3
def test(connection_observer: Optional[ConnectionObserver]) -> Result:
"""
Cycles through 3 possible endpoints,
to attempt to get an IP address for the systemwide connection.
"""
attempt = 0
endpoint_index = 0
endpoint_list = list(API_ENDPOINT_FORMATS.keys())
_ip_test_client = httpx.Client(http2=True, timeout=6)
while attempt < MAX_ATTEMPTS:
endpoint_url = endpoint_list[attempt]
attempt += 1
logger.info(f"Attempt {attempt}: Trying {endpoint_url}")
if connection_observer:
connection_observer.notify("connecting", f"Testing IP. Attempt {attempt}")
try:
raw_response = _ip_test_client.get(endpoint_url)
filtered_response = classify_response(raw_response)
if filtered_response.valid:
ip_address = extract_ip_from_response(
response=filtered_response.data,
api_url=endpoint_url
)
if ip_address is not None:
return Result(valid=True, data=ip_address)
else:
logger.error(f"Skipping {endpoint_url}")
except Exception as e:
logger.error(f"We are NOT error handling this. Skipping {endpoint_url}. Invalid API reply or failed connection {str(e)}")
logger.error(f"We went through {MAX_ATTEMPTS} attempts and could not get a reply.")
return Result(valid=False, error_type=ResultError.CONNECTION, message="Could not connect.")
def extract_ip_from_response(response: dict, api_url: str) -> str | bool:
"""
Purpose:
Extract the IP address from the API's response,
based on which keys that API uses.
Rank:
Helper
On Success:
Returns String of IP address
On Failure,
Returns None
"""
if api_url not in API_ENDPOINT_FORMATS:
logger.critical(f"Systemic Bug! Our local data in extract_ip_from_response doesn't match what we just read for the URL {api_url}.")
return None
desired_key = API_ENDPOINT_FORMATS[api_url]
if desired_key not in response:
logger.error(f"Our local data does NOT match the new format for {api_url}.")
return None
final_value = response[desired_key]
return final_value

View file

@ -8,7 +8,7 @@ from core.services.networking.systemwide.encrypted_proxy import singbox
from core.utils.basic_operations import process_tools
from core.utils.run_commands import run_generic_command
from typing import Callable, cast, Optional
from typing import Callable, cast
import time
KILL_WAIT_TIME = 1.3
@ -45,7 +45,7 @@ def _escalate_kill(pid: int) -> Result:
return Result(valid=force.valid, message=f"Force kill {'succeeded' if force.valid else 'failed'}")
def get_process_id_from_state(current_state: Optional[SystemState]) -> Result:
def get_process_id_from_state(current_state: SystemState) -> Result:
if current_state is None:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing the State itself.")
@ -143,7 +143,7 @@ def _try_shutdown_by_exact_match() -> Result:
return exact_match
def _try_known_pid_from_state(current_state: Optional[SystemState]) -> Result:
def _try_known_pid_from_state(current_state: SystemState) -> Result:
pid_query = get_process_id_from_state(current_state)
if not pid_query.valid:
return pid_query

View file

@ -4,7 +4,6 @@ from core.services.networking.systemwide.general_tools import interface_tools
from core.services.networking.systemwide import killswitch
from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns
from core.utils.basic_operations import pid_tools
from core.services.networking.general_connection_tools import ip
from core.models.Result import Result, ResultError
from core.errors.logger import logger
@ -47,64 +46,17 @@ def set_dns_for_singbox(current_state: SystemState):
raise DNSError(dns_result)
# def _attempt_start_with_retry(profile_id: int, quantity_of_attempts: int = 2) -> Result:
# current_attempt = 0
def launch_singbox_binary(profile_id: int) -> Result:
"""
1) Launch the singbox binary,
2) Test if the process id is working
3) If not, check if the app is running by name. (on any pid)
"""
# while current_attempt < quantity_of_attempts:
# activation_result = singbox.start(profile_id)
activation_result = singbox.start(profile_id)
if not activation_result.valid:
return activation_result
process_id = int(activation_result.data)
logger.info(f"Waiting 2 seconds to see if the process id {process_id} is still alive..")
time.sleep(2)
# Evaluate if running by that exact pid:
active = pid_tools.is_running(pid=process_id, process_name="sing-box")
logger.info(f"Process {process_id} is {active}")
if active:
return Result(valid=True, data=process_id)
else:
logger.error(f"Initial pid test showed it is NOT running for pid {process_id}. Now we're doing a more exhaustive check by app name that might be a different ID")
double_check = pid_tools.get_pid_by_app_name(exact_app_name="sing-box")
if double_check.valid:
logger.info(f"We had the wrong pid!! We had {process_id}, when it's really {double_check.data}")
return Result(valid=True, error_type=ResultError.PROCESS_MISMATCH, data=double_check.data)
else:
error_msg = f"While Singbox might have literally allowed the binary to begin, it's killing the process on id {process_id}"
logger.error(f"[{function_name}] {error_msg}")
return Result(valid=False, error_type=ResultError.PROCESS_GOT_KILLED, message=error_msg, data=process_id)
def _attempt_start_with_retry(profile_id: int, quantity_of_attempts: int = 2) -> Result:
current_attempt = 0
while current_attempt < quantity_of_attempts:
activation_result = launch_singbox_binary(profile_id)
if activation_result.valid:
return activation_result
else:
current_attempt = current_attempt + 1
logger.error(f"[SINGBOX] Attempt {current_attempt} for Singbox Failed. Because: {activation_result.message}. Trying again in two seconds..")
time.sleep(2)
# end of loop / make sure it's not running already..
error_msg = f"Singbox failed to launch after {quantity_of_attempts} attempts"
logger.error(f"{error_msg}, let's double check it's not running already:")
double_check = pid_tools.get_pid_by_app_name(exact_app_name="sing-box")
if double_check.valid:
return double_check
else:
# I'd like to clarify the error_type here, instead of just returning the pid check or an individual attempt.
return Result(valid=False, error_type=PROCESS_WONT_START, message=error_msg)
# if activation_result.valid:
# return activation_result
# else:
# current_attempt = current_attempt + 1
# logger.error(f"[SINGBOX] Attempt {current_attempt} for Singbox Failed. Because: {activation_result.message}. Trying again..")
def end_singbox(
@ -169,6 +121,28 @@ def end_singbox(
return Result(valid=False, error_type=ResultError.SINGBOX, message=error_msg)
def launch_singbox_binary(profile_id: int) -> Result:
activation_result = singbox.start(profile_id)
if not activation_result.valid:
return activation_result
process_id = int(activation_result.data)
logger.info(f"Waiting 2 seconds to see if the process id {process_id} is still alive..")
time.sleep(2)
# Evaluate if running.
active = pid_tools.is_running(pid=process_id, process_name="sing-box")
logger.info(f"Process {process_id} is {active}")
if active:
return Result(valid=True, data=process_id)
else:
error_msg = f"While Singbox might have literally allowed the binary to begin, it's killing the process on id {process_id}"
logger.error(f"[{function_name}] {error_msg}")
return Result(valid=False, error_type=ResultError.PROCESS_GOT_KILLED, message=error_msg, data=process_id)
def start_singbox(
profile_id: int,
@ -203,7 +177,7 @@ def start_singbox(
return killed_pre_existing
# ============= START BINARY =============
launched = _attempt_start_with_retry(profile_id=profile_id, quantity_of_attempts=2)
launched = launch_singbox_binary(profile_id=profile_id)
if not launched.valid:
return launched
@ -228,7 +202,7 @@ def start_singbox(
return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id)
# ============= SETUP STATE =============
# Even if the connection is dead, firewall is off, we want to save the fact we turned Singbox on, before we raise errors.
# Even if firewall is off, we want to save the fact we turned Singbox on, before we raise errors.
logger.info("Setting State JSON with INTENDED firewall & Dns settings")
current_state = SystemStateController.create(
@ -238,23 +212,6 @@ def start_singbox(
process_id=process_id
)
# ============= CONFIRM IP ADDRESS =============
ip_result = ip.test(connection_observer)
if not ip_result.valid:
logger.error(f"IP address check is unable to connect. Killing Singbox pid {process_id}")
closed = orchestrate_closing(current_state=current_state, interface=Constants.SINGBOX_TUN_IF)
raise ConnectionError("Could not connect with IP adddress check.")
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Could not connect with IP adddress check.")
observed_ip_address = ip_result.data
if observed_ip_address == server_ip:
logger.info("Observed IP matches the intended server IP. Proceeding..")
else:
error_msg = f"Critical Leak! IP address check of {observed_ip_address} does NOT match the intended {server_ip}."
logger.error(f"{error_msg} Killing Singbox pid {process_id}")
closed = orchestrate_closing(current_state=current_state, interface=Constants.SINGBOX_TUN_IF)
return Result(valid=False, error_type=ResultError.LEAK_ISSUE, message=error_msg)
# ============= FIREWALL =============
logger.info(f"[{function_name}] Attempting to enable the Firewall for {Constants.SINGBOX_TUN_IF} and {Constants.SINGBOX_INTERNAL_SUBNET}...")
firewall_result = enable_firewall_w_retry( # this function is "generic" as it's protocol neutral