commit 6f6b9715a4b6f023ef20288bc540a6bee1155148 Author: Andrés Eduardo García Márquez Date: Fri Feb 27 21:05:46 2026 -0500 feat: initial release - mDNS publisher and resolver for Termux - mdns-publish.py: publishes movil.local:8022 via Zeroconf - mdns-resolve: raw UDP mDNS resolver (RFC 6762) - ssh-mdns-proxy: SSH ProxyCommand with mDNS/Tailscale/cache fallback - install.sh: automated installer with boot integration Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..33e0091 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# termux-mdns + +mDNS publisher and resolver for Termux (Android). + +Publishes `movil.local` on the local network via Zeroconf so other machines can find the phone by hostname instead of dynamic DHCP IPs. + +## Components + +| Script | Purpose | +|--------|---------| +| `mdns-publish.py` | Publishes `movil.local:8022` as an SSH service via mDNS/Zeroconf | +| `mdns-resolve` | Resolves `.local` hostnames via raw UDP multicast (RFC 6762) | +| `ssh-mdns-proxy` | SSH ProxyCommand with mDNS → Tailscale → cache fallback chain | + +## Install + +```bash +pkg install python +pip install zeroconf +./install.sh +``` + +## Usage + +From any machine on the LAN: +```bash +ssh -p 8022 movil.local +``` + +From the phone to other machines: +```bash +ssh dell # resolves via mdns-resolve → ssh-mdns-proxy +``` + +## Boot + +The installer adds `mdns-publish.py` to `~/.termux/boot/start-services` so it starts automatically when Termux boots. + +## License + +MIT diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..202d8be --- /dev/null +++ b/install.sh @@ -0,0 +1,26 @@ +#!/data/data/com.termux/files/usr/bin/bash +set -e + +BIN="$HOME/.local/bin" +BOOT="$HOME/.termux/boot/start-services" + +mkdir -p "$BIN" + +cp mdns-publish.py mdns-resolve ssh-mdns-proxy "$BIN/" +chmod +x "$BIN/mdns-publish.py" "$BIN/mdns-resolve" "$BIN/ssh-mdns-proxy" + +# Add to boot if not already there +if ! grep -q "mdns-publish" "$BOOT" 2>/dev/null; then + sed -i "/^sshd$/a \\\n# Iniciar mDNS (publica movil.local)\nnohup python ~/.local/bin/mdns-publish.py > /dev/null 2>&1 &" "$BOOT" + echo "Added mdns-publish to boot script" +fi + +# Start if not running +if ! pgrep -f mdns-publish.py > /dev/null; then + nohup python "$BIN/mdns-publish.py" > /dev/null 2>&1 & + echo "mdns-publish started (PID: $!)" +else + echo "mdns-publish already running" +fi + +echo "Install complete" diff --git a/mdns-publish.py b/mdns-publish.py new file mode 100755 index 0000000..920b24e --- /dev/null +++ b/mdns-publish.py @@ -0,0 +1,57 @@ +#!/data/data/com.termux/files/usr/bin/python +"""Publica el servicio SSH como movil.local via mDNS/Zeroconf""" +import socket +import signal +import sys +from zeroconf import ServiceInfo, Zeroconf, IPVersion + +def get_local_ip(): + """Obtiene la IP local del dispositivo""" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + return '127.0.0.1' + +def main(): + hostname = 'movil' + port = 8022 + ip = get_local_ip() + + zc = Zeroconf(ip_version=IPVersion.V4Only) + + # Registrar servicio SSH + info = ServiceInfo( + '_ssh._tcp.local.', + f'{hostname}._ssh._tcp.local.', + addresses=[socket.inet_aton(ip)], + port=port, + properties={'description': 'Termux SSH'}, + server=f'{hostname}.local.', + ) + + print(f'Publicando {hostname}.local -> {ip}:{port}') + zc.register_service(info) + + def signal_handler(sig, frame): + print('Deteniendo mDNS...') + zc.unregister_service(info) + zc.close() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Mantener corriendo + try: + signal.pause() + except: + while True: + import time + time.sleep(3600) + +if __name__ == '__main__': + main() diff --git a/mdns-resolve b/mdns-resolve new file mode 100755 index 0000000..4a069d6 --- /dev/null +++ b/mdns-resolve @@ -0,0 +1,131 @@ +#!/data/data/com.termux/files/usr/bin/python3 +""" +Resuelve hostname.local via mDNS multicast. +Uso: mdns-resolve +Retorna: IP address o exit 1 +""" +import sys +import socket +import struct +import select + +def build_mdns_query(hostname: str) -> bytes: + """Construye query DNS para hostname.""" + # Header: ID=0, Flags=0, QDCOUNT=1, ANCOUNT=0, NSCOUNT=0, ARCOUNT=0 + header = struct.pack('>HHHHHH', 0, 0, 1, 0, 0, 0) + + # Question section + question = b'' + for part in hostname.rstrip('.').split('.'): + question += bytes([len(part)]) + part.encode() + question += b'\x00' # End of name + question += struct.pack('>HH', 1, 1) # Type A, Class IN + + return header + question + +def parse_mdns_response(data: bytes, hostname: str) -> str | None: + """Parsea respuesta mDNS y extrae IP.""" + if len(data) < 12: + return None + + # Skip header, find answers + qdcount = struct.unpack('>H', data[4:6])[0] + ancount = struct.unpack('>H', data[6:8])[0] + + if ancount == 0: + return None + + # Skip questions + offset = 12 + for _ in range(qdcount): + while offset < len(data) and data[offset] != 0: + if data[offset] & 0xc0 == 0xc0: # Compression + offset += 2 + break + offset += data[offset] + 1 + else: + offset += 1 + offset += 4 # Type + Class + + # Parse answers + for _ in range(ancount): + # Skip name (handle compression) + while offset < len(data): + if data[offset] & 0xc0 == 0xc0: + offset += 2 + break + elif data[offset] == 0: + offset += 1 + break + else: + offset += data[offset] + 1 + + if offset + 10 > len(data): + break + + rtype, rclass, ttl, rdlength = struct.unpack('>HHIH', data[offset:offset+10]) + offset += 10 + + # Type A (1) = IPv4 address + if rtype == 1 and rdlength == 4 and offset + 4 <= len(data): + ip = socket.inet_ntoa(data[offset:offset+4]) + return ip + + offset += rdlength + + return None + +def resolve_mdns(hostname: str, timeout: float = 2.0) -> str | None: + """Resuelve hostname via mDNS multicast.""" + if not hostname.endswith('.local'): + hostname += '.local' + + MDNS_ADDR = '224.0.0.251' + MDNS_PORT = 5353 + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setblocking(False) + + # Enviar query + query = build_mdns_query(hostname) + sock.sendto(query, (MDNS_ADDR, MDNS_PORT)) + + # Esperar respuesta + end_time = __import__('time').time() + timeout + while __import__('time').time() < end_time: + ready, _, _ = select.select([sock], [], [], 0.1) + if ready: + try: + data, addr = sock.recvfrom(4096) + ip = parse_mdns_response(data, hostname) + if ip: + sock.close() + return ip + except: + pass + + sock.close() + except Exception as e: + pass + + return None + +def main(): + if len(sys.argv) < 2: + print("Uso: mdns-resolve ", file=sys.stderr) + sys.exit(1) + + hostname = sys.argv[1] + timeout = float(sys.argv[2]) if len(sys.argv) > 2 else 2.0 + + ip = resolve_mdns(hostname, timeout) + if ip: + print(ip) + sys.exit(0) + else: + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/ssh-mdns-proxy b/ssh-mdns-proxy new file mode 100755 index 0000000..8a27135 --- /dev/null +++ b/ssh-mdns-proxy @@ -0,0 +1,39 @@ +#\!/data/data/com.termux/files/usr/bin/bash +# ssh-mdns-proxy: Resolve .local via mDNS with fallbacks, then connect +HOST="$1" +PORT="${2:-22}" +CACHE="$HOME/.ssh/mdns-cache" +QUERY="${HOST%.local}" + +# Alias mapping (short name → mDNS hostname) +declare -A ALIASES=( + [dell]="dell-latitude3400" + [lenovo]="lenovo-ideapad" +) +[[ -n "${ALIASES[$QUERY]}" ]] && QUERY="${ALIASES[$QUERY]}" + +# 1. mDNS resolution +IP=$("$HOME/.local/bin/mdns-resolve" "${QUERY}.local" 2 2>/dev/null) + +# 2. Fallback: Tailscale DNS +if [[ -z "$IP" ]]; then + IP=$(getent hosts "${QUERY}.tailb0bb74.ts.net" 2>/dev/null | awk "{print \$1}") +fi + +# 3. Fallback: cached IP +if [[ -z "$IP" && -f "$CACHE" ]]; then + IP=$(grep "^${QUERY} " "$CACHE" 2>/dev/null | awk "{print \$2}") +fi + +if [[ -z "$IP" ]]; then + echo "Error: cannot resolve ${HOST}" >&2 + exit 1 +fi + +# Save to cache +mkdir -p "$(dirname "$CACHE")" +grep -v "^${QUERY} " "$CACHE" > "${CACHE}.tmp" 2>/dev/null || true +echo "${QUERY} ${IP}" >> "${CACHE}.tmp" +mv "${CACHE}.tmp" "$CACHE" + +exec nc "$IP" "$PORT"