Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 103 additions & 24 deletions pwnagotchi/plugins/default/bt-tether.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,48 +164,125 @@ def on_config_changed(self, config):
if not ("mac" in self.options and re.match(MAC_PTTRN, self.options["mac"])):
logging.error("[BT-Tether] Error with mac address")
return

if not ("phone" in self.options and self.options["phone"].lower() in ["android", "ios"]):
logging.error("[BT-Tether] Phone type not supported")
return
if self.options["phone"].lower() == "android":
address = self.options.get("ip", "192.168.44.2")
gateway = self.options.get("gateway", "192.168.44.1")
elif self.options["phone"].lower() == "ios":
address = self.options.get("ip", "172.20.10.2")
gateway = self.options.get("gateway", "172.20.10.1")
if not re.match(IP_PTTRN, address):
logging.error(f"[BT-Tether] IP error: {address}")
return

# Get IP configuration options
# Supports: 'static' for static IP, 'dhcp' for DHCP
ip_method = self.options.get("ip-method", "").lower()
ip_address = self.options.get("ip", "")
gateway = self.options.get("gateway", "")

# Normalize empty strings and whitespace
ip_address = ip_address.strip() if ip_address else ""
gateway = gateway.strip() if gateway else ""

# Determine IP mode based on configuration
# Priority: explicit ip+gateway > ip-method setting > phone-type defaults
if ip_address and gateway:
# Case 1: Both ip and gateway explicitly provided = static mode
use_static = True
if ip_method == "dhcp":
logging.warning(
"[BT-Tether] ip and gateway provided, ignoring ip-method='dhcp', using static IP"
)
logging.info(f"[BT-Tether] Using static IP: {ip_address}, gateway: {gateway}")

elif ip_method == "dhcp" and not ip_address and not gateway:
# Case 2: DHCP explicitly requested with no static config
use_static = False
logging.info("[BT-Tether] Using DHCP mode for IP configuration")

elif ip_method == "static":
# Case 3: Static mode explicitly requested - use defaults if ip/gateway not fully provided
use_static = True
if self.options["phone"].lower() == "android":
ip_address = ip_address or "192.168.44.2"
gateway = gateway or "192.168.44.1"
else: # iOS
ip_address = ip_address or "172.20.10.2"
gateway = gateway or "172.20.10.1"
logging.info(f"[BT-Tether] Using static IP with defaults: {ip_address}, gateway: {gateway}")

else:
# Case 4: No explicit config - fall back to static IP defaults (backwards compatible)
use_static = True
if self.options["phone"].lower() == "android":
ip_address = "192.168.44.2"
gateway = "192.168.44.1"
else: # iOS uses consistent 172.20.10.0/28 subnet
ip_address = "172.20.10.2"
gateway = "172.20.10.1"
logging.info(f"[BT-Tether] Using static IP defaults: {ip_address}, gateway: {gateway}")

# Validate IP addresses for static mode
if use_static:
if not re.match(IP_PTTRN, ip_address):
logging.error(f"[BT-Tether] Invalid IP address: {ip_address}")
return
if not re.match(IP_PTTRN, gateway):
logging.error(f"[BT-Tether] Invalid gateway: {gateway}")
return

self.phone_name = self.options["phone-name"] + " Network"
self.mac = self.options["mac"]

# DNS handling - required for static mode, optional for DHCP mode
dns = self.options.get("dns", "8.8.8.8 1.1.1.1")
if not re.match(DNS_PTTRN, dns):
if dns == "":
logging.error(f"[BT-Tether] Empty DNS setting")
if use_static:
if not dns or not re.match(DNS_PTTRN, dns):
logging.error(f"[BT-Tether] DNS required for static IP mode: '{dns}'")
return
dns = re.sub(r"[\s,;]+", " ", dns).strip()
elif dns:
# DHCP mode - DNS override is optional
if re.match(DNS_PTTRN, dns):
dns = re.sub(r"[\s,;]+", " ", dns).strip()
else:
logging.error(f"[BT-Tether] Wrong DNS setting: '{dns}'")
return
dns = re.sub("[\s,;]+", " ", dns).strip() # DNS cleaning
dns = "" # Invalid/empty DNS in DHCP mode = use phone's DNS

try:
# Configure connection. Metric is set to 200 to prefer connection over USB
self.nmcli(
[
if not use_static:
# DHCP configuration - let NetworkManager handle IP assignment
nmcli_args = [
"connection", "modify", f"{self.phone_name}",
"connection.type", "bluetooth",
"bluetooth.type", "panu",
"bluetooth.bdaddr", f"{self.mac}",
"connection.autoconnect", "yes",
"connection.autoconnect-retries", "0",
"ipv4.method", "manual",
"ipv4.dns", f"{dns}",
"ipv4.addresses", f"{address}/24",
"ipv4.gateway", f"{gateway}",
"ipv4.method", "auto",
"ipv4.addresses", "", # Clear any stale static addresses
"ipv4.gateway", "", # Clear any stale gateway
"ipv4.route-metric", "200",
]
)
# Allow optional DNS override even in DHCP mode
if dns:
nmcli_args.extend(["ipv4.dns", f"{dns}"])
else:
nmcli_args.extend(["ipv4.dns", ""]) # Clear stale DNS, use DHCP-provided
self.nmcli(nmcli_args)
logging.info("[BT-Tether] NetworkManager configured for DHCP")
else:
# Static IP configuration
self.nmcli(
[
"connection", "modify", f"{self.phone_name}",
"connection.type", "bluetooth",
"bluetooth.type", "panu",
"bluetooth.bdaddr", f"{self.mac}",
"connection.autoconnect", "yes",
"connection.autoconnect-retries", "0",
"ipv4.method", "manual",
"ipv4.dns", f"{dns}",
"ipv4.addresses", f"{ip_address}/24",
"ipv4.gateway", f"{gateway}",
"ipv4.route-metric", "200",
]
)
logging.info(f"[BT-Tether] NetworkManager configured for static IP: {ip_address}")
# Configure Device to autoconnect
self.nmcli([
"device", "set", f"{self.mac}",
Expand Down Expand Up @@ -238,6 +315,8 @@ def on_unload(self, ui):
with ui._lock:
ui.remove_element("bluetooth")
try:
logging.info(f"[BT-Tether] Disconnecting from {self.phone_name}")

self.nmcli(["connection", "down", f"{self.phone_name}"])
except Exception as e:
logging.error(f"[BT-Tether] Failed to disconnect from device: {e}")
Expand Down Expand Up @@ -325,4 +404,4 @@ def on_webhook(self, path, request):
device=device,
connection=connection,
)
abort(404)
abort(404)