3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

どうしてもSSH 22番をパブリックIPでリッスンしたいのでポートノックWeb APIを作る

Last updated at Posted at 2025-07-12

 今どきTailscaleとかを繋いでしまえば、22番を直接パブリックIPにつなぎに行く必要なんてないわけなんですが、それでもどうしてもTailscaleが使えない環境とかではやらざるを得ないこともあるわけですね。

iptablesを設定してみよう

/etc/iptables/iptables.rules

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -i tailscale0 -j ACCEPT
-A INPUT -p tcp --dport 22 -m recent --rcheck --seconds 180 --name knock --rsource -j ACCEPT
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
COMMIT

こんな感じにします。 --name knock とすることで、 /proc/net/xt_recent/knock というファイルが生えます。
ここに、 +1.2.3.4 と書き込むと 1.2.3.4 からは22番が180秒間見えるというわけです。

Web APIをつくる

まずユーザー webknock というものを作り、 /home/webknock をつくります。

useradd webknock
mkdir ~webknock
chown webknock:webknock ~webknock
sudo -u webknock -i

venvを作ります。

python -m venv venv
source venv/bin/activate

ここで requirements.txt を書きます。

flask
python-dotenv
gunicorn
requests

書けたら pip install -r requirements.txt

で、こんな感じの app.py をこしらえます。

from flask import Flask, request, abort, jsonify
import os
from dotenv import load_dotenv
import re
import requests

load_dotenv()

XT_RECENT_FILE = "/proc/net/xt_recent/knock"
API_KEY = os.environ.get("KNOCK_API_KEY")
DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL")

app = Flask(__name__)

ipv4_re = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")

def extract_ipv4(forwarded_for, remote_addr):
    # X-Forwarded-For内のすべてのIPv4アドレスを列挙し最初のものを返す
    if forwarded_for:
        matches = ipv4_re.findall(forwarded_for)
        if matches:
            return matches[0]
    # remote_addrにもマッチするものがあれば返す
    if remote_addr and ipv4_re.match(remote_addr):
        return remote_addr
    return None

@app.route("/knock", methods=["POST"])
def knock():
    key = request.headers.get("X-Api-Key")
    if not API_KEY or key != API_KEY:
        abort(403)

    forwarded_for = request.headers.get("X-Forwarded-For")
    ipv4 = extract_ipv4(forwarded_for, request.remote_addr)
    if not ipv4:
        abort(400)  # IPv4アドレスが取得できない場合はBad Request

    try:
        with open(XT_RECENT_FILE, "w") as f:
            f.write(f"+{ipv4}\n")
    except Exception as e:
        return str(e), 500

    if DISCORD_WEBHOOK_URL:
        try:
            requests.post(
                DISCORD_WEBHOOK_URL,
                json={"content": f"✅ Knock accepted: {ipv4}"},
                timeout=5
            )
        except Exception as e:
            app.logger.warning(f"Discord Webhook通知失敗: {e}")

    return jsonify({"status": "OK", "added_ip": ipv4}), 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

正しいAPIリクエストをすると、接続元のIPv4アドレスを /proc/net/xt_recent/knock に書き込み、Discord通知してくれるというわけです。
.env も必要です。

KNOCK_API_KEY=kamisatoayaka
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/***

これをsystemdで起動していきます。

/etc/systemd/system/webknock.service

Description=WebKnock Gunicorn Service (unix socket)
After=network.target iptables.service

[Service]
WorkingDirectory=/home/webknock
ExecStartPre=/bin/chown webknock:webknock /proc/net/xt_recent/knock
ExecStartPre=/usr/bin/chmod 755 /home/webknock
ExecStart=/usr/bin/sudo -u webknock /home/webknock/venv/bin/gunicorn --bind unix:/home/webknock/webknock.sock --access-logfile - app:app
ExecStartPost=/home/webknock/chmod_sock.sh
Restart=always

[Install]
WantedBy=multi-user.target

ExecStartでsudo使うのがあんまり気持ちよくないですが、まあこれが一番簡単だったので⋯。
これによって、 /home/webknock/webknock.sock というUNIXドメインソケットができます。

Cloudflare Tunnel (cloudflared) でリッスンする

流石にSSLの面倒を見るのは嫌なので、APIの公開はCloudflareに面倒を見てもらいます。

適当なフォルダに docker-compose.yml を書きます。

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    command: tunnel --no-autoupdate run --token TOKENTOKENTOKENTOKEN
    volumes:
      - /home/webknock/webknock.sock:/home/webknock/webknock.sock
    restart: unless-stopped

スクリーンショット 2025-07-12 12.21.02.png

こんな感じにTunnelを設定します。

つないでみる

これでポートノック完成です。

curl -X POST \             
  -H "X-Api-Key: kamisatoayaka" \
  https://sha.bugyo.inazuma/knock
ssh 160.100.101.102

ご安全に。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?