Compare commits
No commits in common. "c3b50460cd0dd87067fdd9163d561b09facc2479" and "9d0d14046790d48ce4a5b320ad54ac2593dafed1" have entirely different histories.
c3b50460cd
...
9d0d140467
4 changed files with 36 additions and 177 deletions
|
|
@ -19,7 +19,6 @@ class ResultError(Enum):
|
||||||
SUBSCRIPTION = "subscription"
|
SUBSCRIPTION = "subscription"
|
||||||
PROCESS_GOT_KILLED = "process_got_killed"
|
PROCESS_GOT_KILLED = "process_got_killed"
|
||||||
PROCESS_WONT_START = "process_wont_start"
|
PROCESS_WONT_START = "process_wont_start"
|
||||||
PROCESS_MISMATCH = "process_mismatch"
|
|
||||||
NMCLI = "nmcli_issues"
|
NMCLI = "nmcli_issues"
|
||||||
FIREWALL = "firewall"
|
FIREWALL = "firewall"
|
||||||
CLIENT_DNS = "client_dns"
|
CLIENT_DNS = "client_dns"
|
||||||
|
|
@ -27,7 +26,6 @@ class ResultError(Enum):
|
||||||
INTERFACE = "interface"
|
INTERFACE = "interface"
|
||||||
TIMEOUT = "timeout"
|
TIMEOUT = "timeout"
|
||||||
INVALID_API_REPLY = "invalid_api_reply"
|
INVALID_API_REPLY = "invalid_api_reply"
|
||||||
LEAK_ISSUE = "leak_issue"
|
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ from core.services.networking.systemwide.encrypted_proxy import singbox
|
||||||
from core.utils.basic_operations import process_tools
|
from core.utils.basic_operations import process_tools
|
||||||
from core.utils.run_commands import run_generic_command
|
from core.utils.run_commands import run_generic_command
|
||||||
|
|
||||||
from typing import Callable, cast, Optional
|
from typing import Callable, cast
|
||||||
import time
|
import time
|
||||||
|
|
||||||
KILL_WAIT_TIME = 1.3
|
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'}")
|
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:
|
if current_state is None:
|
||||||
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing the State itself.")
|
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
|
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)
|
pid_query = get_process_id_from_state(current_state)
|
||||||
if not pid_query.valid:
|
if not pid_query.valid:
|
||||||
return pid_query
|
return pid_query
|
||||||
|
|
|
||||||
|
|
@ -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 import killswitch
|
||||||
from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns
|
from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns
|
||||||
from core.utils.basic_operations import pid_tools
|
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.models.Result import Result, ResultError
|
||||||
from core.errors.logger import logger
|
from core.errors.logger import logger
|
||||||
|
|
@ -47,64 +46,17 @@ def set_dns_for_singbox(current_state: SystemState):
|
||||||
raise DNSError(dns_result)
|
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:
|
# while current_attempt < quantity_of_attempts:
|
||||||
"""
|
# activation_result = singbox.start(profile_id)
|
||||||
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)
|
|
||||||
"""
|
|
||||||
|
|
||||||
activation_result = singbox.start(profile_id)
|
# if activation_result.valid:
|
||||||
if not activation_result.valid:
|
# return activation_result
|
||||||
return activation_result
|
# else:
|
||||||
|
# current_attempt = current_attempt + 1
|
||||||
process_id = int(activation_result.data)
|
# logger.error(f"[SINGBOX] Attempt {current_attempt} for Singbox Failed. Because: {activation_result.message}. Trying again..")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def end_singbox(
|
def end_singbox(
|
||||||
|
|
@ -169,6 +121,28 @@ def end_singbox(
|
||||||
return Result(valid=False, error_type=ResultError.SINGBOX, message=error_msg)
|
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(
|
def start_singbox(
|
||||||
profile_id: int,
|
profile_id: int,
|
||||||
|
|
@ -203,7 +177,7 @@ def start_singbox(
|
||||||
return killed_pre_existing
|
return killed_pre_existing
|
||||||
|
|
||||||
# ============= START BINARY =============
|
# ============= 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:
|
if not launched.valid:
|
||||||
return launched
|
return launched
|
||||||
|
|
||||||
|
|
@ -228,7 +202,7 @@ def start_singbox(
|
||||||
return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id)
|
return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id)
|
||||||
|
|
||||||
# ============= SETUP STATE =============
|
# ============= 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")
|
logger.info("Setting State JSON with INTENDED firewall & Dns settings")
|
||||||
current_state = SystemStateController.create(
|
current_state = SystemStateController.create(
|
||||||
|
|
@ -238,23 +212,6 @@ def start_singbox(
|
||||||
process_id=process_id
|
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 =============
|
# ============= FIREWALL =============
|
||||||
logger.info(f"[{function_name}] Attempting to enable the Firewall for {Constants.SINGBOX_TUN_IF} and {Constants.SINGBOX_INTERNAL_SUBNET}...")
|
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
|
firewall_result = enable_firewall_w_retry( # this function is "generic" as it's protocol neutral
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue