Add one-click installation script for Headscale clients
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
53c1f45e02
commit
8bd6b1f400
458
PROXMOX_WEBSOCKET_INTEGRATION_PLAN.md
Normal file
458
PROXMOX_WEBSOCKET_INTEGRATION_PLAN.md
Normal file
@ -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
|
||||||
|
<!-- templates/vnc_console.html -->
|
||||||
|
<script src="/static/js/socket.io.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Flask-SocketIO 연결
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
// VNC 연결 설정
|
||||||
|
socket.emit('vnc_connect', {
|
||||||
|
vm_id: {{ vmid }},
|
||||||
|
node: '{{ node }}'
|
||||||
|
});
|
||||||
|
|
||||||
|
// noVNC WebSocket URL을 Flask 프록시로 변경
|
||||||
|
const websocketUrl = `ws://localhost:5002/socket.io/`;
|
||||||
|
|
||||||
|
// RFB 연결
|
||||||
|
const rfb = new RFB(canvas, websocketUrl, {
|
||||||
|
wsProtocols: ['base64']
|
||||||
|
});
|
||||||
|
|
||||||
|
// 데이터 중계 설정
|
||||||
|
socket.on('vnc_data', function(data) {
|
||||||
|
// Proxmox에서 온 데이터를 noVNC로 전달
|
||||||
|
rfb._sock.rQshiftBytes(data.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
rfb.addEventListener('connect', function() {
|
||||||
|
// noVNC에서 보낸 데이터를 Proxmox로 전달
|
||||||
|
rfb._sock.on('message', function(data) {
|
||||||
|
socket.emit('vnc_data', data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 장점
|
||||||
|
- ✅ 완전한 제어 가능
|
||||||
|
- ✅ 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/<session_id>')
|
||||||
|
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 프록시 구현 시작
|
||||||
@ -18,7 +18,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- headscale-net
|
- headscale-net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "nc -z localhost 8080 || exit 1"]
|
test: ["CMD", "/ko-app/headscale", "version"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
445
quick-install.sh
Executable file
445
quick-install.sh
Executable file
@ -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 <node> # 다른 노드와 연결 테스트"
|
||||||
|
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 "$@"
|
||||||
Loading…
Reference in New Issue
Block a user