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 <noreply@anthropic.com>
This commit is contained in:
commit
6f6b9715a4
|
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
#!/data/data/com.termux/files/usr/bin/python3
|
||||||
|
"""
|
||||||
|
Resuelve hostname.local via mDNS multicast.
|
||||||
|
Uso: mdns-resolve <hostname.local>
|
||||||
|
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 <hostname.local>", 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()
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue