今どき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
こんな感じにTunnelを設定します。
つないでみる
これでポートノック完成です。
curl -X POST \
-H "X-Api-Key: kamisatoayaka" \
https://sha.bugyo.inazuma/knock
ssh 160.100.101.102
ご安全に。