RasPiやNanoPiをdebug terminalなしで立ち上げる

# はじめに
RasPiやNanoPiのSBCでLinuxをInstallするとき、最終的に必要がないのに設定のためにRS-232CやKeyboard & Mouse, HDMIを繋ぐことがあります。面倒ですね。
ここでは、コンソールやDebug TerminalなしにWindowsでSD cardを焼いて、いきなりネットからLogin出来る方法を紹介します。Windows 10/11で操作します。Linuxでもできます。MacOSもたぶん行けます。

# IP Addressを探す
結局は。SBCがDHCPDでどこに割り当てられたかが不明なだけです。それさえわかればtelnetで接続できます。IPを調査するツールを作りました。

プログラムは以下の通り。
``` ipquest.py
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# ipquest.py
# Copyright (c) 2023 Shigeru Makino
# MIT License
from scapy.all import ARP, Ether, srp
import sys
import subprocess
import ipaddress
from icmplib import multiping
import netifaces
import os
import platform
import telnetlib
import paramiko
import getpass


def mac2ip(mac, net):
    arp = ARP(pdst=net, hwdst="ff:ff:ff:ff:ff:ff")
    ether = Ether(dst="ff:ff:ff:ff:ff:ff")
    packet = ether / arp
    for i in range(0,5):
        result = srp(packet, timeout=5, verbose=0)[0]
        if result:
            break
    for sent, received in result:
        if received.psrc and received.hwsrc == mac:
            return received.psrc


def connect_host(ipaddress):
    port = 22
    command = "uname -a"
    u = "pi"
    username = input(f"Username: (Default:{u}):")
    if not username:
        username = u
    password = getpass.getpass()
    tp = paramiko.Transport((ipaddress, int(port)))
    try:
        tp.connect(username=username, password=password, hostkey=None)
    except Exception:
        tp.close()
        raise SystemExit("Bad username or password.")
    ch = tp.open_channel("session")
    ch.exec_command(command)

    while not ch.closed:
        if ch.recv_stderr_ready:
            print(ch.recv_stderr(1024).decode("utf-8)"))
        if ch.recv_ready:
            print(ch.recv(1024).decode("utf-8"))


def t_telnet(host):
    port = 22
    timeout = 5
    try:
        tn = telnetlib.Telnet(host, port, timeout)
    except Exception as e:
        print(f"\nCould not connect to {host}:{port}. Error: {e}")
    else:
        print(f"\nTry connect to {host}:{port}")
        tn.write(b"\r\n")
        ans = tn.read_all()
        print(f'Get Answer: {ans.decode("utf-8")}')
        tn.close()


def is_admin():
    if platform.system() == "Windows":
        try:
            return subprocess.check_output("net session", shell=True)
        except subprocess.CalledProcessError:
            return False
    else:
        return os.getuid() == 0


def get_mac(ip):
    arp = ARP(pdst=ip)
    ether = Ether(dst="ff:ff:ff:ff:ff:ff")
    packet = ether / arp
    result = srp(packet, timeout=15, verbose=0)[0]
    if result:
        return result[0][1].hwsrc
    else:
        return None


def get_iface():
    interfaces = netifaces.interfaces()
    ifaces = []
    for iface in interfaces:
        iface_addresses = netifaces.ifaddresses(iface)
        if netifaces.AF_INET in iface_addresses:
            ip_address = iface_addresses[netifaces.AF_INET][0]["addr"]
            if ip_address != "127.0.0.1":
                ifaces.append((iface, ip_address))
    return ifaces


if not is_admin():
    print("Administrative privileges are required to run this program.")
    input("Enter and Exit")
    sys.exit()

ansi_cls = '\033[2J\033[H'
print(f"""
{ansi_cls}
This program checks which IPs of the hosts connected in the first
      scan are disconnected in the second scan."""
)

ifaces = get_iface()
print("\nI have inferface(s):")
for iface in ifaces:
    print(iface)

iface_no = len(ifaces)
if iface_no < 1:
    ans = 'n'
elif iface_no == 1:
    ans = 'y'
else:
    for iface in ifaces:
        ans = input(f"Can I scaning {iface} (y/N) ?")
        if ans == "y":
            break

if ans != 'y':
    print('There is no interface which you choiced, bye !')
    sys.exit()

gateway = iface[1]
network = ipaddress.IPv4Network(f"{gateway}/24", strict=False)
address = [str(ip) for ip in network.hosts()]
print(f"Now, I am scanning {network}, wait a moment")
broadcast = f'{network.broadcast_address}/24'
alive_hosts = []
ip_mac_dict = {}
scan1 = multiping(address)
for host in scan1:
    if host.is_alive:
        ip = host.address
        macaddr = get_mac(ip)
        ip_mac_dict[ip] = macaddr
        alive_hosts.append(ip)
        ent = {ip: macaddr}
        ip_mac_dict.update(ent)
        print(f"Alive:{ip} [{macaddr}]")
input(
    """
First scan is finished.
    Unplug the LAN of the host in question, Enter to continue: """
)
scan2 = multiping(alive_hosts)
for lost in scan2:
    if not lost.is_alive:
        ip = lost.address
        macaddr = ip_mac_dict[ip]
        print(f"Down:{ip} [{macaddr}]")
        input("Re-Plug the LAN & Enter:")
        ipnew = mac2ip(macaddr, broadcast)
        print(f"IP changed or the same: {ip} -> {ipnew}")
        if ipnew:
            ip = ipnew
        t_telnet(ip)
        connect_host(ip)
        print("Finished")
        sys.exit(0)
print("No downed host, Bye !")

```
このツールを管理者権限で走らせます。
```
This program checks which IPs of the hosts connected in the first
      scan are disconnected in the second scan.

I have inferface(s):
('{8B42FD22-5C81-4150-8037-471FD5C4FC57}', '192.168.200.1')
('{05D60E80-B223-4085-8095-C06B96913E78}', '169.254.67.210')
('{D7DBAD4F-05FB-4362-B5AF-3D351C139181}', '192.168.1.9')
Can I scaning ('{8B42FD22-5C81-4150-8037-471FD5C4FC57}', '192.168.200.1') (y/N) ?
Can I scaning ('{05D60E80-B223-4085-8095-C06B96913E78}', '169.254.67.210') (y/N) ?
Can I scaning ('{D7DBAD4F-05FB-4362-B5AF-3D351C139181}', '192.168.1.9') (y/N) ?y
Now, I am scanning 192.168.1.0/24, wait a moment
Alive:192.168.1.1 [d4:c1:c8:63:8c:04]
Alive:192.168.1.3 [e8:51:77:99:f3:ec]
Alive:192.168.1.4 [8c:f7:10:4b:38:8c]
Alive:192.168.1.7 [3e:3a:1b:9e:7d:b8]
Alive:192.168.1.9 [c8:34:8e:55:ab:43]

First scan is finished.
    Unplug the LAN of the host in question, Enter to continue:
Down:192.168.1.7 [3e:3a:1b:9e:7d:b8]
Re-Plug the LAN & Enter:
IP changed or the same: 192.168.1.7 -> None

Could not connect to 192.168.1.7:22. Error: timed out
Username: (Default:pi):pi
Password:

Linux NanoPi-NEO4 4.19.193 #3 SMP PREEMPT Thu Aug 25 21:05:15 CST 2022 aarch64 GNU/Linux

Finished
PS C:\users\admin>
```
はじめにpingに応答したHostとそのmac addressをリストし、後半に応答が無くなったHostを示します。これがあればtelnet/ssdでつなぐことができますし、hdcpdを設定してIPを固定することも容易です。

でも、いくつかの環境整備が要ります。
#### Windowsの場合
無料のVC++開発環境を[Download](https://visualstudio.microsoft.com/ja/thank-you-downloading-visual-studio/?sku=Community&channel=Release&version=VS2022&source=VSLandingPage&passive=false&cid=2030)から取ってPythonとVC++が使えるようにします。

次に[Npcap](https://npcap.com/#download)をInstallします。IP Packetを操作し、ARPを利用できるようにします。
#### Debian/Ubuntuの場合
gcc, python-dev環境をinstallします。
```
apt install gcc
apt install python-dev
```
#### その他のOS
その他のOSでもC++の開発、python libraryを構築するヘッダー（Python開発環境）があれば実行可能です。ない場合pipがエラーを吐きますので、下記「OS共通」を実行し、そのエラーを丸ごとChatGPTに送ると教えてくれると思います。

# OS共通
pipでパッケージを集めます。
```
pip install icmplib
pip install netifaces
pip install scapy
pip install paramiko
```
します。数分間の時間がかかります。pipが自動的にC++でソースをコンパイルし、インストールします。

# 起動します

該当のNanoPiをLANにつなぎ電源を入れてからこのプログラムを走らせます。
Windowsでは管理者モードのPowerShell、Linux系OSではrootで実行してください。
準備ができたらEnterを押すと１回目のScanをし、応答したホストを表示します。
NanoPiのLANを抜いてEnterを押すと前回応答したものだけをScanします。これで応答しなくなったものがNanoPiのIPです。

# Telnet / SSH で繋ぐ
こうなれば[PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html)や[Tera term](https://ja.osdn.net/projects/ttssh2/)で接続するだけです。
pi/piで入れるのでまず、パスワードを変更します。

# すぐやるべきこと
1. piのパスワードを変える
1. sudo passwd root でrootにパスワードを変える
1. sudo adduser YOU であなたのアカウントを作る
1. sudo deluser pi でアカウントを消す
1. root loginを禁止にする
1. sudo禁止にする

#### piのパスワード
ネットにつないでいるのですからNanoPiでもRaspiでも世界の果てからアッタックさされる危険性があります。まずパスワードを変えます。

#### 新たにアカウントを作る
あなたのアカウントを独自に作ってください。
```
adduser <YOU>
```
#### piを消す
user名とpasswdが組で防衛してます。knownなpiを使い続けるのはセキュアではありません。
```
deluser pi
```

#### root login
sshdを設定します。

```
echo 'PermitRootLogin no' > /etc/ssh/sshd_config.d/PermetRootLogin.cfg
```
sshd サービスを再起動します。
```
systemctl restart sshd
```

# まとめ
1. SBC LinuxはIP addressさえわかればtelnetできる
1. ipquest.pyはDHCPが割り当てたIPを探るプログラムである
1. Telnet に成功したら直ちにセキュリティ対策をとる
1. 可能ならば、設定中は外部から遮断されたプライベートはネットワークが好ましい。