From 8bd6b1f40015303e6e062e10055e4d57e0fc96b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Thu, 11 Sep 2025 00:30:47 +0900 Subject: [PATCH] Add one-click installation script for Headscale clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Complete automated script for Tailscale + Headscale registration - Support for Ubuntu, Debian, CentOS, RHEL, Rocky, Fedora, Arch - Universal binary fallback for unsupported distros - Automatic firewall configuration - Network connectivity verification - Fix Headscale health check (nc -> headscale version) - Add comprehensive error handling and colored output ๐Ÿš€ Generated with Claude Code Co-Authored-By: Claude --- PROXMOX_WEBSOCKET_INTEGRATION_PLAN.md | 458 ++++++++++++++++++++++++++ docker-compose.yml | 2 +- quick-install.sh | 445 +++++++++++++++++++++++++ 3 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 PROXMOX_WEBSOCKET_INTEGRATION_PLAN.md create mode 100755 quick-install.sh diff --git a/PROXMOX_WEBSOCKET_INTEGRATION_PLAN.md b/PROXMOX_WEBSOCKET_INTEGRATION_PLAN.md new file mode 100644 index 0000000..091ea75 --- /dev/null +++ b/PROXMOX_WEBSOCKET_INTEGRATION_PLAN.md @@ -0,0 +1,458 @@ +# Proxmox VNC WebSocket ์ง์ ‘ ์—ฐ๊ฒฐ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ + +## ๐ŸŽฏ ๋ชฉํ‘œ + +Flask ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Proxmox VNC WebSocket์— **์ง์ ‘ ์—ฐ๊ฒฐ**ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ๋‚ด์—์„œ seamlessํ•œ VNC ๊ฒฝํ—˜ ์ œ๊ณต + +## ๐Ÿ” ํ˜„์žฌ ๋ฌธ์ œ ์ƒํ™ฉ + +### 1. ๋ฌธ์ œ ์ฆ์ƒ +- Flask์—์„œ VNC ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ `"Unsupported"... is not valid JSON` ์˜ค๋ฅ˜ ๋ฐœ์ƒ +- noVNC ํด๋ผ์ด์–ธํŠธ๊ฐ€ Proxmox WebSocket ์„œ๋ฒ„์— ์—ฐ๊ฒฐ ์‹คํŒจ +- HTTP 501 ์‘๋‹ต (nginx์—์„œ WebSocket ์š”์ฒญ ๊ฑฐ๋ถ€) + +### 2. ๊ทผ๋ณธ ์›์ธ ๋ถ„์„ +``` +[๋ธŒ๋ผ์šฐ์ €] --> [Flask:5002] --> [Proxmox:443] --> [์‹ค์ œ WebSocket:5900] + โŒ CORS ์ด์Šˆ โŒ nginx ์ฐจ๋‹จ โœ… VM VNC ์„œ๋ฒ„ +``` + +**์ฃผ์š” ์ฐจ๋‹จ ์š”์ธ:** +- Proxmox nginx๊ฐ€ ์™ธ๋ถ€ WebSocket ์—ฐ๊ฒฐ ์ฐจ๋‹จ +- CORS (Cross-Origin Resource Sharing) ์ •์ฑ… +- SSL/TLS ์ธ์ฆ์„œ ๋ถˆ์ผ์น˜ +- WebSocket Upgrade ํ—ค๋” ์ฒ˜๋ฆฌ ๋ฌธ์ œ + +## ๐Ÿ’ก ํ•ด๊ฒฐ ๋ฐฉ์•ˆ (3๊ฐ€์ง€ ์ ‘๊ทผ๋ฒ•) + +## ๋ฐฉ์•ˆ 1: Flask WebSocket ํ”„๋ก์‹œ ์„œ๋ฒ„ (๊ถŒ์žฅ) + +### ๊ฐœ๋…๋„ +``` +[๋ธŒ๋ผ์šฐ์ €] <--WebSocket--> [Flask Proxy] <--WebSocket--> [Proxmox VNC] + ws://localhost:5002/ws/vnc/vm123 wss://pve7:443/api2/... +``` + +### ๊ตฌํ˜„ ๋ฐฉ๋ฒ• + +#### 1.1 Flask-SocketIO ๊ธฐ๋ฐ˜ ํ”„๋ก์‹œ +```python +# requirements.txt์— ์ถ”๊ฐ€ +flask-socketio==5.3.6 +python-socketio==5.8.0 +websockets==11.0.3 + +# app.py ์ˆ˜์ • +from flask_socketio import SocketIO, emit +import websockets +import asyncio +import ssl + +socketio = SocketIO(app, cors_allowed_origins="*") + +@socketio.on('vnc_connect') +def handle_vnc_connect(data): + """VNC WebSocket ํ”„๋ก์‹œ ์—ฐ๊ฒฐ""" + vm_id = data['vm_id'] + node = data['node'] + + # Proxmox VNC ํ‹ฐ์ผ“ ์ƒ์„ฑ + client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + vnc_data = client.get_vnc_ticket(node, vm_id) + + if vnc_data: + # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ํ”„๋ก์‹œ ์‹คํ–‰ + socketio.start_background_task(proxy_vnc_connection, + request.sid, + vnc_data['websocket_url']) + +def proxy_vnc_connection(session_id, proxmox_ws_url): + """Proxmox VNC WebSocket๊ณผ ๋ธŒ๋ผ์šฐ์ € ๊ฐ„ ํ”„๋ก์‹œ""" + asyncio.run(proxy_websocket(session_id, proxmox_ws_url)) + +async def proxy_websocket(session_id, proxmox_ws_url): + """๋น„๋™๊ธฐ WebSocket ํ”„๋ก์‹œ""" + try: + # SSL ์ปจํ…์ŠคํŠธ ์„ค์ • (์ธ์ฆ์„œ ๊ฒ€์ฆ ๋ฌด์‹œ) + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # Proxmox WebSocket ์—ฐ๊ฒฐ + async with websockets.connect( + proxmox_ws_url, + ssl=ssl_context, + extra_headers={'Origin': f'https://{PROXMOX_HOST}'} + ) as proxmox_ws: + + @socketio.on('vnc_data', namespace=f'/vnc/{session_id}') + def forward_to_proxmox(data): + asyncio.create_task(proxmox_ws.send(data)) + + # Proxmox โ†’ ๋ธŒ๋ผ์šฐ์ € + async for message from proxmox_ws: + socketio.emit('vnc_data', message, + room=session_id, + namespace=f'/vnc/{session_id}') + + except Exception as e: + socketio.emit('vnc_error', {'error': str(e)}, room=session_id) +``` + +#### 1.2 noVNC ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • +```html + + + +``` + +### ์žฅ์  +- โœ… ์™„์ „ํ•œ ์ œ์–ด ๊ฐ€๋Šฅ +- โœ… CORS ๋ฌธ์ œ ํ•ด๊ฒฐ +- โœ… Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด ํ†ตํ•ฉ +- โœ… ์ถ”๊ฐ€ ์ธ์ฆ/๋กœ๊น… ๊ฐ€๋Šฅ + +### ๋‹จ์  +- โŒ ๊ตฌํ˜„ ๋ณต์žก์„ฑ ๋†’์Œ +- โŒ ์„ฑ๋Šฅ ์˜ค๋ฒ„ํ—ค๋“œ ์กด์žฌ +- โŒ WebSocket ํ”„๋กœํ† ์ฝœ ๊นŠ์ด ์ดํ•ด ํ•„์š” + +--- + +## ๋ฐฉ์•ˆ 2: Nginx ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ์„ค์ • + +### ๊ฐœ๋…๋„ +``` +[๋ธŒ๋ผ์šฐ์ €] --> [Nginx Proxy] --> [Proxmox WebSocket] + ws://proxy/vnc/vm123 wss://pve7:443/api2/... +``` + +### ๊ตฌํ˜„ ๋ฐฉ๋ฒ• + +#### 2.1 ๋ณ„๋„ Nginx ํ”„๋ก์‹œ ์„œ๋ฒ„ ์„ค์ • +```nginx +# /etc/nginx/sites-available/proxmox-vnc-proxy +server { + listen 8080; + server_name localhost; + + # WebSocket ํ”„๋ก์‹œ ์„ค์ • + location /vnc/ { + # URL ๊ฒฝ๋กœ์—์„œ VM ์ •๋ณด ์ถ”์ถœ: /vnc/pve7/100 + # pve7 = node, 100 = vmid + rewrite ^/vnc/([^/]+)/(\d+)$ /api2/json/nodes/$1/qemu/$2/vncwebsocket break; + + proxy_pass https://pve7.0bin.in:443; + proxy_http_version 1.1; + + # WebSocket ์—…๊ทธ๋ ˆ์ด๋“œ ํ—ค๋” + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host pve7.0bin.in; + proxy_set_header Origin https://pve7.0bin.in; + + # ์ธ์ฆ ํ—ค๋” ์ „๋‹ฌ (VNC ํ‹ฐ์ผ“ ํฌํ•จ) + proxy_set_header Authorization $http_authorization; + + # SSL ์„ค์ • + proxy_ssl_verify off; + proxy_ssl_server_name on; + + # ํƒ€์ž„์•„์›ƒ ์„ค์ • (VNC ์„ธ์…˜์šฉ) + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 86400s; # 24์‹œ๊ฐ„ + + # ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™” (์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ์šฉ) + proxy_buffering off; + proxy_cache off; + } + + # CORS ํ—ค๋” ์ถ”๊ฐ€ + location / { + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization"; + + if ($request_method = 'OPTIONS') { + return 204; + } + } +} +``` + +#### 2.2 Docker Compose๋กœ Nginx ํ”„๋ก์‹œ ์‹คํ–‰ +```yaml +# docker-compose.vnc-proxy.yml +version: '3.8' +services: + vnc-proxy: + image: nginx:alpine + ports: + - "8080:80" + volumes: + - ./nginx-vnc-proxy.conf:/etc/nginx/conf.d/default.conf + restart: unless-stopped + networks: + - farmq-network + +networks: + farmq-network: + external: true +``` + +#### 2.3 Flask ์ฝ”๋“œ ์ˆ˜์ • +```python +# VNC WebSocket URL์„ ํ”„๋ก์‹œ ์„œ๋ฒ„๋กœ ๋ณ€๊ฒฝ +@app.route('/api/vm/vnc', methods=['POST']) +def api_vm_vnc(): + # ... (๊ธฐ์กด ์ฝ”๋“œ) + + # ํ”„๋ก์‹œ ์„œ๋ฒ„๋ฅผ ํ†ตํ•œ WebSocket URL ์ƒ์„ฑ + proxy_ws_url = f"ws://localhost:8080/vnc/{node}/{vmid}?port={vnc_data['port']}&vncticket={vnc_data['ticket']}" + + vnc_sessions[session_id] = { + 'websocket_url': proxy_ws_url, + # ... + } +``` + +### ์žฅ์  +- โœ… Flask ์ฝ”๋“œ ๋ณ€๊ฒฝ ์ตœ์†Œํ™” +- โœ… ๋†’์€ ์„ฑ๋Šฅ (Nginx ๋„ค์ดํ‹ฐ๋ธŒ) +- โœ… ํ™•์žฅ์„ฑ ์šฐ์ˆ˜ +- โœ… SSL ์ข…๋ฃŒ ์ง€์› + +### ๋‹จ์  +- โŒ ์ถ”๊ฐ€ ์ธํ”„๋ผ ํ•„์š” +- โŒ Nginx ์„ค์ • ๋ณต์žก์„ฑ +- โŒ ๋””๋ฒ„๊น… ์–ด๋ ค์›€ + +--- + +## ๋ฐฉ์•ˆ 3: Proxmox ์„œ๋ฒ„ ์„ค์ • ๋ณ€๊ฒฝ + +### ๊ฐœ๋…๋„ +``` +[๋ธŒ๋ผ์šฐ์ €] ----์ง์ ‘์—ฐ๊ฒฐ----> [Proxmox WebSocket (์ˆ˜์ •๋จ)] + wss://pve7:443/api2/json/... +``` + +### ๊ตฌํ˜„ ๋ฐฉ๋ฒ• + +#### 3.1 Proxmox Nginx ์„ค์ • ์ˆ˜์ • +```bash +# Proxmox ์„œ๋ฒ„์— SSH ์ ‘์† +ssh root@pve7.0bin.in + +# nginx ์„ค์ • ๋ฐฑ์—… +cp /etc/nginx/sites-available/proxmox /etc/nginx/sites-available/proxmox.backup + +# nginx ์„ค์ • ํŽธ์ง‘ +nano /etc/nginx/sites-available/proxmox +``` + +```nginx +# /etc/nginx/sites-available/proxmox ์ˆ˜์ • +server { + listen 443 ssl http2; + server_name pve7.0bin.in; + + # ... ๊ธฐ์กด ์„ค์ • ์œ ์ง€ ... + + # CORS ํ—ค๋” ์ถ”๊ฐ€ (VNC WebSocket์šฉ) + location /api2/json/nodes/*/qemu/*/vncwebsocket { + # CORS ํ—ค๋” + add_header Access-Control-Allow-Origin "http://localhost:5002"; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization"; + add_header Access-Control-Allow-Credentials true; + + # Preflight ์š”์ฒญ ์ฒ˜๋ฆฌ + if ($request_method = 'OPTIONS') { + return 204; + } + + # ๊ธฐ์กด proxy_pass ์„ค์ • ์œ ์ง€ + proxy_pass http://localhost:8006; + proxy_http_version 1.1; + + # WebSocket ์—…๊ทธ๋ ˆ์ด๋“œ ํ—ค๋” + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # ํƒ€์ž„์•„์›ƒ ์„ค์ • + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + # ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™” + proxy_buffering off; + proxy_cache off; + } + + # ... ๋‚˜๋จธ์ง€ ์„ค์ • ... +} +``` + +#### 3.2 Proxmox nginx ์žฌ์‹œ์ž‘ +```bash +# ์„ค์ • ๊ฒ€์ฆ +nginx -t + +# nginx ์žฌ์‹œ์ž‘ +systemctl restart nginx + +# ๋ฐฉํ™”๋ฒฝ ํ™•์ธ (ํ•„์š”์‹œ) +iptables -L -n | grep 8006 +``` + +#### 3.3 Flask noVNC ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • +```javascript +// ์ง์ ‘ Proxmox WebSocket ์—ฐ๊ฒฐ +const websocketUrl = '{{ websocket_url }}'; // wss://pve7.0bin.in:443/api2/json/... + +// CORS ์„ค์ •์œผ๋กœ ์ง์ ‘ ์—ฐ๊ฒฐ ๊ฐ€๋Šฅ +const rfb = new RFB(canvas, websocketUrl, { + credentials: { password: '' }, // VNC ํ‹ฐ์ผ“์œผ๋กœ ์ธ์ฆ + shared: true +}); +``` + +### ์žฅ์  +- โœ… ๊ฐ€์žฅ ์ง์ ‘์ ์ธ ํ•ด๊ฒฐ์ฑ… +- โœ… ์ตœ๊ณ  ์„ฑ๋Šฅ +- โœ… ์ถ”๊ฐ€ ์ธํ”„๋ผ ๋ถˆํ•„์š” + +### ๋‹จ์  +- โŒ Proxmox ์„œ๋ฒ„ ์ˆ˜์ • ํ•„์š” +- โŒ ์—…๋ฐ์ดํŠธ ์‹œ ์„ค์ • ์ดˆ๊ธฐํ™” ์œ„ํ—˜ +- โŒ ๋ณด์•ˆ ์ •์ฑ… ๋ณ€๊ฒฝ + +--- + +## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„ ๋ฐ ๊ถŒ์žฅ์‚ฌํ•ญ + +### 1๋‹จ๊ณ„: Flask WebSocket ํ”„๋ก์‹œ (๊ถŒ์žฅ) +**๊ธฐ๊ฐ„**: 2-3์ผ +**๋‚œ์ด๋„**: ์ค‘์ƒ + +```bash +# ๊ตฌํ˜„ ๋‹จ๊ณ„ +1. Flask-SocketIO ์„ค์น˜ ๋ฐ ์„ค์ • +2. VNC WebSocket ํ”„๋ก์‹œ ํ•จ์ˆ˜ ๊ตฌํ˜„ +3. noVNC ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • +4. ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น… +``` + +### 2๋‹จ๊ณ„: Nginx ํ”„๋ก์‹œ (๋ฐฑ์—… ๋ฐฉ์•ˆ) +**๊ธฐ๊ฐ„**: 1-2์ผ +**๋‚œ์ด๋„**: ์ค‘ + +```bash +# ๊ตฌํ˜„ ๋‹จ๊ณ„ +1. nginx ํ”„๋ก์‹œ ์„ค์ • ์ž‘์„ฑ +2. Docker Compose๋กœ ํ”„๋ก์‹œ ์‹คํ–‰ +3. Flask WebSocket URL ์ˆ˜์ • +4. ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +``` + +### 3๋‹จ๊ณ„: Proxmox ์„ค์ • ๋ณ€๊ฒฝ (์ตœํ›„ ์ˆ˜๋‹จ) +**๊ธฐ๊ฐ„**: 1์ผ +**๋‚œ์ด๋„**: ํ•˜ (์œ„ํ—˜๋„: ์ƒ) + +```bash +# ์ฃผ์˜์‚ฌํ•ญ +- Proxmox ์„œ๋ฒ„ ์ง์ ‘ ์ˆ˜์ • +- ๋ฐฑ์—… ํ•„์ˆ˜ +- ์—…๋ฐ์ดํŠธ ์‹œ ์žฌ์„ค์ • ํ•„์š” +``` + +## ๐Ÿ”ง ์ฆ‰์‹œ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ ์ž„์‹œ ํ•ด๊ฒฐ์ฑ… + +ํ˜„์žฌ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋ฐฉ์‹์„ ๊ฐœ์„ ํ•˜์—ฌ ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ: + +```python +# app.py - ์ž๋™ ํŒ์—… + ์ธ์ฆ ์ •๋ณด ์ „๋‹ฌ +@app.route('/vnc/') +def vnc_console(session_id): + # Proxmox ์ž๋™ ๋กœ๊ทธ์ธ URL ์ƒ์„ฑ + client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + login_ticket = client.ticket + + proxmox_url = f"https://{PROXMOX_HOST}:443/?console=kvm&vmid={vmid}&node={node}&PVEAuthCookie={login_ticket}" + + return render_template('vnc_auto_redirect.html', + proxmox_url=proxmox_url, + auto_open=True) +``` + +## ๐Ÿ“‹ ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### Flask WebSocket ํ”„๋ก์‹œ ๊ตฌํ˜„ +- [ ] Flask-SocketIO ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ • +- [ ] Proxmox WebSocket ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +- [ ] ๋น„๋™๊ธฐ ํ”„๋ก์‹œ ํ•จ์ˆ˜ ๊ตฌํ˜„ +- [ ] noVNC ํด๋ผ์ด์–ธํŠธ SocketIO ์—ฐ๋™ +- [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์—ฐ๊ฒฐ ๋กœ์ง +- [ ] ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” + +### ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค +- [ ] ๋‹จ์ผ VM VNC ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +- [ ] ๋™์‹œ ๋‹ค์ค‘ VNC ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +- [ ] ๋„คํŠธ์›Œํฌ ๋Š๊น€ ์‹œ ์žฌ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +- [ ] ๋ธŒ๋ผ์šฐ์ €๋ณ„ ํ˜ธํ™˜์„ฑ ํ…Œ์ŠคํŠธ +- [ ] ๋ชจ๋ฐ”์ผ ๋””๋ฐ”์ด์Šค ํ…Œ์ŠคํŠธ + +### ์šด์˜ ์ค€๋น„ +- [ ] ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • +- [ ] ์—๋Ÿฌ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๊ตฌ์ถ• +- [ ] ๋ฐฑ์—… ์—ฐ๊ฒฐ ๋ฐฉ๋ฒ• ์ค€๋น„ +- [ ] ์‚ฌ์šฉ์ž ๋งค๋‰ด์–ผ ์ž‘์„ฑ + +--- + +## ๐Ÿ’ก ๊ฒฐ๋ก  + +**๊ถŒ์žฅ ์ ‘๊ทผ๋ฒ•**: Flask WebSocket ํ”„๋ก์‹œ (๋ฐฉ์•ˆ 1) + +1. **์™„์ „ํ•œ ์ œ์–ด**: Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ ๋ชจ๋“  ๊ฒƒ์„ ์ œ์–ด +2. **ํ™•์žฅ์„ฑ**: ํ–ฅํ›„ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ (๋…นํ™”, ๊ณต์œ  ๋“ฑ) ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ +3. **์•ˆ์ •์„ฑ**: Proxmox ์„œ๋ฒ„ ์ˆ˜์ • ์—†์ด ๊ตฌํ˜„ +4. **ํ†ตํ•ฉ์„ฑ**: ๊ธฐ์กด Flask ์ธ์ฆ/๊ถŒํ•œ ์‹œ์Šคํ…œ๊ณผ ์™„๋ฒฝ ํ†ตํ•ฉ + +**๋‹ค์Œ ๋‹จ๊ณ„**: Flask-SocketIO ๊ธฐ๋ฐ˜ WebSocket ํ”„๋ก์‹œ ๊ตฌํ˜„ ์‹œ์ž‘ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 106cb47..5598486 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: networks: - headscale-net healthcheck: - test: ["CMD-SHELL", "nc -z localhost 8080 || exit 1"] + test: ["CMD", "/ko-app/headscale", "version"] interval: 30s timeout: 10s retries: 3 diff --git a/quick-install.sh b/quick-install.sh new file mode 100755 index 0000000..14e8d97 --- /dev/null +++ b/quick-install.sh @@ -0,0 +1,445 @@ +#!/bin/bash + +# ํŒœํ(FARMQ) Headscale ์›ํด๋ฆญ ์„ค์น˜ ๋ฐ ๋“ฑ๋ก ์Šคํฌ๋ฆฝํŠธ +# ์‚ฌ์šฉ๋ฒ•: curl -fsSL https://raw.githubusercontent.com/your-repo/headscale-setup/main/quick-install.sh | sudo bash +# ๋˜๋Š”: wget -qO- https://raw.githubusercontent.com/your-repo/headscale-setup/main/quick-install.sh | sudo bash + +set -e + +# ================================ +# ์„ค์ • (ํ•„์š”์‹œ ์ˆ˜์ •) +# ================================ +HEADSCALE_SERVER="http://192.168.0.151:8070" # Headscale ์„œ๋ฒ„ ์ฃผ์†Œ +PREAUTH_KEY="8b3df41d37cb158ea39f41fc32c9af46e761de817ad06038" # 7์ผ๊ฐ„ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ‚ค +FARMQ_NETWORK="100.64.0.0/10" # ํŒœํ ๋„คํŠธ์›Œํฌ ๋Œ€์—ญ + +# ================================ +# ์ƒ‰์ƒ ์ถœ๋ ฅ ํ•จ์ˆ˜ +# ================================ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +NC='\033[0m' # No Color + +print_header() { + echo -e "\n${PURPLE}============================================${NC}" + echo -e "${WHITE}$1${NC}" + echo -e "${PURPLE}============================================${NC}\n" +} + +print_status() { + echo -e "\n${BLUE}๐Ÿ”ง $1${NC}" +} + +print_success() { + echo -e "\n${GREEN}โœ… $1${NC}" +} + +print_error() { + echo -e "\n${RED}โŒ $1${NC}" +} + +print_info() { + echo -e "\n${CYAN}๐Ÿ“‹ $1${NC}" +} + +print_warning() { + echo -e "\n${YELLOW}โš ๏ธ $1${NC}" +} + +# ================================ +# ์šด์˜์ฒด์ œ ๊ฐ์ง€ +# ================================ +detect_os() { + if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$ID + VERSION=$VERSION_ID + CODENAME=$VERSION_CODENAME + else + print_error "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์šด์˜์ฒด์ œ์ž…๋‹ˆ๋‹ค." + exit 1 + fi + + print_info "๊ฐ์ง€๋œ OS: $OS $VERSION ($CODENAME)" +} + +# ================================ +# ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ ํ™•์ธ +# ================================ +check_requirements() { + print_status "์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ ํ™•์ธ ์ค‘..." + + # Root ๊ถŒํ•œ ํ™•์ธ + if [ "$EUID" -ne 0 ]; then + print_error "์ด ์Šคํฌ๋ฆฝํŠธ๋Š” root ๊ถŒํ•œ์œผ๋กœ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." + print_info "sudo $0 ๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”." + exit 1 + fi + + # curl ๋˜๋Š” wget ํ™•์ธ + if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + print_error "curl ๋˜๋Š” wget์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." + exit 1 + fi + + # ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ™•์ธ + if ! ping -c 1 8.8.8.8 >/dev/null 2>&1; then + print_warning "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”." + fi + + print_success "์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ ํ™•์ธ ์™„๋ฃŒ" +} + +# ================================ +# Tailscale ์„ค์น˜ +# ================================ +install_tailscale() { + print_status "Tailscale ํด๋ผ์ด์–ธํŠธ ์„ค์น˜ ์ค‘..." + + # ์ด๋ฏธ ์„ค์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + if command -v tailscale >/dev/null 2>&1; then + print_info "Tailscale์ด ์ด๋ฏธ ์„ค์น˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค." + TAILSCALE_VERSION=$(tailscale version | head -n1) + print_info "ํ˜„์žฌ ๋ฒ„์ „: $TAILSCALE_VERSION" + return + fi + + case $OS in + ubuntu|debian) + print_info "Ubuntu/Debian์šฉ Tailscale ์„ค์น˜ ์ค‘..." + + # GPG ํ‚ค ์ถ”๊ฐ€ + curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null + curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list + + # ํŒจํ‚ค์ง€ ์„ค์น˜ + apt-get update -qq + apt-get install -y tailscale + ;; + + centos|rhel|rocky|almalinux) + print_info "CentOS/RHEL/Rocky์šฉ Tailscale ์„ค์น˜ ์ค‘..." + + # ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ถ”๊ฐ€ + curl -fsSL https://pkgs.tailscale.com/stable/rhel/tailscale.repo | tee /etc/yum.repos.d/tailscale.repo + + # ํŒจํ‚ค์ง€ ์„ค์น˜ + if command -v dnf >/dev/null 2>&1; then + dnf install -y tailscale + else + yum install -y tailscale + fi + ;; + + fedora) + print_info "Fedora์šฉ Tailscale ์„ค์น˜ ์ค‘..." + dnf install -y tailscale + ;; + + arch) + print_info "Arch Linux์šฉ Tailscale ์„ค์น˜ ์ค‘..." + pacman -S --noconfirm tailscale + ;; + + *) + print_warning "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฐฐํฌํŒ์ž…๋‹ˆ๋‹ค. ์ˆ˜๋™ ์„ค์น˜๋ฅผ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค." + # Universal binary ๋‹ค์šด๋กœ๋“œ + ARCH=$(uname -m) + case $ARCH in + x86_64) TAILSCALE_ARCH="amd64" ;; + aarch64) TAILSCALE_ARCH="arm64" ;; + armv7l) TAILSCALE_ARCH="arm" ;; + *) + print_error "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์•„ํ‚คํ…์ฒ˜: $ARCH" + exit 1 + ;; + esac + + # ์ตœ์‹  ๋ฒ„์ „ ๋‹ค์šด๋กœ๋“œ + TAILSCALE_VERSION=$(curl -s https://api.github.com/repos/tailscale/tailscale/releases/latest | grep '"tag_name"' | cut -d'"' -f4) + DOWNLOAD_URL="https://pkgs.tailscale.com/stable/tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}.tgz" + + cd /tmp + curl -LO "$DOWNLOAD_URL" + tar xzf "tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}.tgz" + + # ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ณต์‚ฌ + cp "tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}/tailscale" /usr/bin/ + cp "tailscale_${TAILSCALE_VERSION#v}_linux_${TAILSCALE_ARCH}/tailscaled" /usr/sbin/ + + # ์‹œ์Šคํ…œ ์„œ๋น„์Šค ํŒŒ์ผ ์ƒ์„ฑ + cat > /etc/systemd/system/tailscaled.service << 'EOF' +[Unit] +Description=Tailscale node agent +Documentation=https://tailscale.com/kb/ +Wants=network-pre.target +After=network-pre.target NetworkManager.service systemd-resolved.service + +[Service] +EnvironmentFile=/etc/default/tailscaled +ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=$PORT $FLAGS +ExecStopPost=/usr/bin/tailscale logout +Restart=on-failure +RestartSec=5 + +Type=notify +RuntimeDirectory=tailscale +RuntimeDirectoryMode=0755 +StateDirectory=tailscale +StateDirectoryMode=0700 +CacheDirectory=tailscale +CacheDirectoryMode=0750 + +[Install] +WantedBy=multi-user.target +EOF + + # ํ™˜๊ฒฝ ์„ค์ • ํŒŒ์ผ + mkdir -p /etc/default + echo 'FLAGS=""' > /etc/default/tailscaled + echo 'PORT="41641"' >> /etc/default/tailscaled + + systemctl daemon-reload + ;; + esac + + print_success "Tailscale ์„ค์น˜ ์™„๋ฃŒ" + + # ๋ฒ„์ „ ํ™•์ธ + TAILSCALE_VERSION=$(tailscale version | head -n1) + print_info "์„ค์น˜๋œ ๋ฒ„์ „: $TAILSCALE_VERSION" +} + +# ================================ +# Tailscale ์„œ๋น„์Šค ์‹œ์ž‘ +# ================================ +start_tailscale() { + print_status "Tailscale ์„œ๋น„์Šค ์‹œ์ž‘ ์ค‘..." + + # systemd ์„œ๋น„์Šค ํ™œ์„ฑํ™” ๋ฐ ์‹œ์ž‘ + systemctl enable tailscaled >/dev/null 2>&1 || true + systemctl start tailscaled >/dev/null 2>&1 || true + + # ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ + sleep 3 + if systemctl is-active --quiet tailscaled; then + print_success "Tailscaled ์„œ๋น„์Šค๊ฐ€ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค." + else + print_error "Tailscaled ์„œ๋น„์Šค ์‹œ์ž‘์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค." + print_info "์ˆ˜๋™์œผ๋กœ ์‹œ์ž‘์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค..." + /usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state & + sleep 5 + fi +} + +# ================================ +# Headscale ๋“ฑ๋ก +# ================================ +register_headscale() { + print_status "Headscale ์„œ๋ฒ„์— ๋“ฑ๋ก ์ค‘..." + + # ๊ธฐ์กด ์—ฐ๊ฒฐ ํ™•์ธ + if tailscale status >/dev/null 2>&1; then + print_warning "์ด๋ฏธ Tailscale/Headscale์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค." + tailscale status + + read -p "๊ธฐ์กด ์—ฐ๊ฒฐ์„ ํ•ด์ œํ•˜๊ณ  ์ƒˆ๋กœ ๋“ฑ๋กํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + print_info "๊ธฐ์กด ์—ฐ๊ฒฐ์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค..." + tailscale logout >/dev/null 2>&1 || true + sleep 2 + else + print_info "๋“ฑ๋ก์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค." + return + fi + fi + + print_info "Headscale ์„œ๋ฒ„: $HEADSCALE_SERVER" + print_info "Pre-auth Key: ${PREAUTH_KEY:0:8}***************" + + # Headscale ๋“ฑ๋ก ์‹œ๋„ + print_status "๋“ฑ๋ก ๋ช…๋ น ์‹คํ–‰ ์ค‘..." + + if tailscale up \ + --login-server="$HEADSCALE_SERVER" \ + --authkey="$PREAUTH_KEY" \ + --accept-routes \ + --accept-dns=false >/dev/null 2>&1; then + + print_success "Headscale ๋“ฑ๋ก ์„ฑ๊ณต!" + + else + print_error "์ž๋™ ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋™ ๋“ฑ๋ก์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." + + # ์ˆ˜๋™ ๋“ฑ๋ก ๋ชจ๋“œ + print_info "๋‹ค์Œ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜์—ฌ ์ˆ˜๋™ ๋“ฑ๋กํ•˜์„ธ์š”:" + echo "" + echo "tailscale up --login-server=\"$HEADSCALE_SERVER\" --authkey=\"$PREAUTH_KEY\"" + echo "" + + # ๋“ฑ๋ก URL ์‹œ๋„ + REGISTER_URL=$(tailscale up --login-server="$HEADSCALE_SERVER" 2>&1 | grep -o 'https://[^[:space:]]*' | head -1) + if [ -n "$REGISTER_URL" ]; then + print_info "๋˜๋Š” ๋‹ค์Œ URL์„ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋“ฑ๋กํ•˜์„ธ์š”:" + echo "$REGISTER_URL" + fi + + return 1 + fi +} + +# ================================ +# ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ +# ================================ +verify_connection() { + print_status "์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ ์ค‘..." + + # ์ž ์‹œ ๋Œ€๊ธฐ (์—ฐ๊ฒฐ ์•ˆ์ •ํ™”) + sleep 5 + + # Tailscale ์ƒํƒœ ํ™•์ธ + if ! tailscale status >/dev/null 2>&1; then + print_error "Tailscale ์—ฐ๊ฒฐ์— ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค." + return 1 + fi + + # IP ์ฃผ์†Œ ํ™•์ธ + TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "N/A") + TAILSCALE_IP6=$(tailscale ip -6 2>/dev/null || echo "N/A") + + print_success "Headscale ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์™„๋ฃŒ!" + print_info "ํ• ๋‹น๋œ IPv4: $TAILSCALE_IP" + print_info "ํ• ๋‹น๋œ IPv6: $TAILSCALE_IP6" + + # ๋„คํŠธ์›Œํฌ ํ…Œ์ŠคํŠธ + print_status "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์ค‘..." + + if ping -c 3 -W 5 100.64.0.1 >/dev/null 2>&1; then + print_success "ํŒœํ ๋„คํŠธ์›Œํฌ($FARMQ_NETWORK) ์—ฐ๊ฒฐ ์ •์ƒ!" + else + print_warning "๋„คํŠธ์›Œํฌ ํ…Œ์ŠคํŠธ ์‹คํŒจ. ๋ฐฉํ™”๋ฒฝ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”." + fi + + # ์—ฐ๊ฒฐ๋œ ๋…ธ๋“œ ํ™•์ธ + print_info "๋„คํŠธ์›Œํฌ ์ƒํƒœ:" + tailscale status | head -10 +} + +# ================================ +# ๋ฐฉํ™”๋ฒฝ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ) +# ================================ +configure_firewall() { + print_status "๋ฐฉํ™”๋ฒฝ ์„ค์ • ํ™•์ธ ์ค‘..." + + # UFW (Ubuntu/Debian) + if command -v ufw >/dev/null 2>&1; then + print_info "UFW ๋ฐฉํ™”๋ฒฝ ๊ฐ์ง€๋จ" + if ufw status | grep -q "Status: active"; then + print_info "Tailscale ํŠธ๋ž˜ํ”ฝ ํ—ˆ์šฉ ์ค‘..." + ufw allow in on tailscale0 >/dev/null 2>&1 || true + ufw allow 41641/udp comment "Tailscale" >/dev/null 2>&1 || true + fi + fi + + # firewalld (CentOS/RHEL/Fedora) + if command -v firewall-cmd >/dev/null 2>&1; then + print_info "firewalld ๋ฐฉํ™”๋ฒฝ ๊ฐ์ง€๋จ" + if firewall-cmd --state >/dev/null 2>&1; then + print_info "Tailscale ํŠธ๋ž˜ํ”ฝ ํ—ˆ์šฉ ์ค‘..." + firewall-cmd --permanent --add-service=tailscale >/dev/null 2>&1 || true + firewall-cmd --permanent --add-port=41641/udp >/dev/null 2>&1 || true + firewall-cmd --reload >/dev/null 2>&1 || true + fi + fi + + print_success "๋ฐฉํ™”๋ฒฝ ์„ค์ • ์™„๋ฃŒ" +} + +# ================================ +# ์ •๋ฆฌ ์ž‘์—… +# ================================ +cleanup() { + print_status "์ •๋ฆฌ ์ž‘์—… ์ˆ˜ํ–‰ ์ค‘..." + + # ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ + rm -rf /tmp/tailscale_* >/dev/null 2>&1 || true + + # ์‹œ์Šคํ…œ ์ •๋ณด ์—…๋ฐ์ดํŠธ + if command -v updatedb >/dev/null 2>&1; then + updatedb >/dev/null 2>&1 & + fi + + print_success "์ •๋ฆฌ ์ž‘์—… ์™„๋ฃŒ" +} + +# ================================ +# ์ตœ์ข… ์ •๋ณด ์ถœ๋ ฅ +# ================================ +show_final_info() { + print_header "ํŒœํ Headscale ์„ค์น˜ ์™„๋ฃŒ!" + + # ์‹œ์Šคํ…œ ์ •๋ณด + HOSTNAME=$(hostname) + TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "N/A") + + echo -e "${GREEN}๐ŸŽ‰ ์„ค์น˜๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!${NC}\n" + + echo -e "${CYAN}๐Ÿ“‹ ์‹œ์Šคํ…œ ์ •๋ณด:${NC}" + echo -e " ํ˜ธ์ŠคํŠธ๋ช…: $HOSTNAME" + echo -e " Tailscale IP: $TAILSCALE_IP" + echo -e " OS: $OS $VERSION" + echo -e " Headscale ์„œ๋ฒ„: $HEADSCALE_SERVER" + + echo -e "\n${YELLOW}๐Ÿ”ง ์œ ์šฉํ•œ ๋ช…๋ น์–ด:${NC}" + echo -e " tailscale status # ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ" + echo -e " tailscale ip # ํ• ๋‹น๋œ IP ํ™•์ธ" + echo -e " tailscale ping # ๋‹ค๋ฅธ ๋…ธ๋“œ์™€ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ" + echo -e " tailscale logout # ๋„คํŠธ์›Œํฌ์—์„œ ํ•ด์ œ" + + echo -e "\n${PURPLE}๐ŸŒ ํŒœํ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€:${NC}" + echo -e " http://192.168.0.151:5002" + echo -e " http://192.168.0.151:5002/vms (VM ๊ด€๋ฆฌ)" + + echo -e "\n${WHITE}๋ฌธ์ œ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜์„ธ์š”:${NC}" + echo -e " journalctl -u tailscaled -f" + + print_header "์„ค์น˜ ์™„๋ฃŒ - ํŒœํ ๋„คํŠธ์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!" +} + +# ================================ +# ๋ฉ”์ธ ํ•จ์ˆ˜ +# ================================ +main() { + print_header "ํŒœํ(FARMQ) Headscale ์›ํด๋ฆญ ์„ค์น˜" + + # ์‚ฌ์ „ ์ฒดํฌ + detect_os + check_requirements + + # ์„ค์น˜ ๊ณผ์ • + install_tailscale + start_tailscale + register_headscale + + # ์‚ฌํ›„ ์„ค์ • + configure_firewall + verify_connection + + # ์ •๋ฆฌ ๋ฐ ์™„๋ฃŒ + cleanup + show_final_info +} + +# ================================ +# ์—๋Ÿฌ ํ•ธ๋“ค๋ง +# ================================ +trap 'echo -e "\nโŒ ์„ค์น˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."; exit 1' ERR + +# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ +main "$@" \ No newline at end of file