VNC WebSocket 연결 문제 - 브라우저 보안 정책으로 인한 미해결 상태
WebSocket 1006 오류로 인해 브라우저에서 VNC 연결 실패 - 서버 환경에서는 연결 가능하나 브라우저 보안 정책으로 차단 - 역방향 프록시 솔루션 문서화 완료 - 추후 nginx 프록시 구현 필요 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1dc09101cc
commit
ac620a0e15
343
Multi-Proxmox-Management-System.md
Normal file
343
Multi-Proxmox-Management-System.md
Normal file
@ -0,0 +1,343 @@
|
||||
# 다중 Proxmox 호스트 관리 시스템 기획서
|
||||
|
||||
## 🎯 프로젝트 개요
|
||||
|
||||
약국별 독립적인 Proxmox 서버를 중앙에서 통합 관리하는 시스템 구축. Headscale 네트워크를 통해 여러 Proxmox 호스트에 접속하여 VM을 관리하고 VNC 원격 접속을 제공.
|
||||
|
||||
## 📋 현재 상황 분석
|
||||
|
||||
### 기존 구현 현황
|
||||
- ✅ 단일 Proxmox 서버 (`pve7.0bin.in`) 연동 완료
|
||||
- ✅ VNC WebSocket 직접 연결 구현
|
||||
- ✅ VNC 인증 실패 자동 해결 시스템
|
||||
- ✅ VM 목록 조회 및 상태 확인
|
||||
- ✅ VM 시작/정지 기능
|
||||
|
||||
### 확장 요구사항
|
||||
- 약국 수: 100개 (예상)
|
||||
- Proxmox 호스트: 약국별 1개씩 (총 100대)
|
||||
- 네트워크: Headscale VPN으로 연결된 사설 IP (예: 100.64.0.x)
|
||||
- 인증: 모든 Proxmox에서 동일한 `root@pam` 계정 사용
|
||||
|
||||
## 🏗️ 시스템 아키텍처
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Central Management Web UI │
|
||||
│ (farmq-admin) │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
│ Headscale VPN │
|
||||
│ Network │
|
||||
└─────────┬─────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
|
||||
│약국 A │ │약국 B │ │약국 C │
|
||||
│Proxmox│ │Proxmox│ │Proxmox│
|
||||
│100.64.│ │100.64.│ │100.64.│
|
||||
│0.10 │ │0.11 │ │0.12 │
|
||||
└───┬───┘ └───┬───┘ └───┬───┘
|
||||
│ │ │
|
||||
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
|
||||
│VM들 │ │VM들 │ │VM들 │
|
||||
│- 키오스│ │- POS │ │- 서버 │
|
||||
│- POS │ │- 키오스│ │- 백업 │
|
||||
└───────┘ └───────┘ └───────┘
|
||||
```
|
||||
|
||||
## 📊 데이터베이스 설계
|
||||
|
||||
### 1. Proxmox 호스트 관리 테이블
|
||||
```sql
|
||||
CREATE TABLE proxmox_hosts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pharmacy_id INTEGER NOT NULL,
|
||||
pharmacy_name VARCHAR(255) NOT NULL,
|
||||
host_ip VARCHAR(15) NOT NULL, -- 100.64.0.x
|
||||
host_port INTEGER DEFAULT 443,
|
||||
username VARCHAR(50) DEFAULT 'root@pam',
|
||||
password VARCHAR(255) NOT NULL,
|
||||
status ENUM('online', 'offline', 'maintenance') DEFAULT 'offline',
|
||||
last_check TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(pharmacy_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 2. VM 인벤토리 테이블
|
||||
```sql
|
||||
CREATE TABLE vm_inventory (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
proxmox_host_id INTEGER NOT NULL,
|
||||
vmid INTEGER NOT NULL,
|
||||
vm_name VARCHAR(255) NOT NULL,
|
||||
vm_type ENUM('kiosk', 'pos', 'server', 'backup', 'other') DEFAULT 'other',
|
||||
node_name VARCHAR(100) NOT NULL,
|
||||
status ENUM('running', 'stopped', 'suspended') DEFAULT 'stopped',
|
||||
cpu_cores INTEGER,
|
||||
memory_mb INTEGER,
|
||||
disk_gb INTEGER,
|
||||
ip_address VARCHAR(15),
|
||||
description TEXT,
|
||||
last_sync TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (proxmox_host_id) REFERENCES proxmox_hosts(id)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. VNC 세션 확장 테이블
|
||||
```sql
|
||||
CREATE TABLE vnc_sessions_extended (
|
||||
session_id VARCHAR(36) PRIMARY KEY,
|
||||
proxmox_host_id INTEGER NOT NULL,
|
||||
vmid INTEGER NOT NULL,
|
||||
vm_name VARCHAR(255) NOT NULL,
|
||||
websocket_url TEXT NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
last_refresh TIMESTAMP,
|
||||
FOREIGN KEY (proxmox_host_id) REFERENCES proxmox_hosts(id)
|
||||
);
|
||||
```
|
||||
|
||||
## 🔧 주요 기능 구현 계획
|
||||
|
||||
### 1. Proxmox 호스트 관리
|
||||
- **호스트 등록/수정/삭제**
|
||||
- 약국 정보와 연동하여 Proxmox 호스트 정보 관리
|
||||
- IP, 포트, 인증 정보 저장
|
||||
|
||||
- **호스트 상태 모니터링**
|
||||
- 주기적 헬스체크 (ping, API 연결 확인)
|
||||
- 오프라인 호스트 알림 및 상태 표시
|
||||
|
||||
- **보안 관리**
|
||||
- 패스워드 암호화 저장
|
||||
- API 토큰 관리 옵션 제공
|
||||
|
||||
### 2. 통합 VM 관리 대시보드
|
||||
- **멀티 호스트 VM 목록**
|
||||
```
|
||||
약국A (100.64.0.10) - 온라인
|
||||
├── VM 101: Kiosk-1 (실행중)
|
||||
├── VM 102: POS-System (정지됨)
|
||||
└── VM 103: Backup-Server (실행중)
|
||||
|
||||
약국B (100.64.0.11) - 오프라인
|
||||
├── 연결 불가
|
||||
|
||||
약국C (100.64.0.12) - 온라인
|
||||
├── VM 201: Main-Server (실행중)
|
||||
└── VM 202: Kiosk-Terminal (실행중)
|
||||
```
|
||||
|
||||
- **통합 검색 및 필터링**
|
||||
- 약국별, VM 타입별, 상태별 필터링
|
||||
- VM 이름 및 IP 주소 검색
|
||||
- 상태별 대시보드 (전체 실행중/정지됨 VM 수)
|
||||
|
||||
- **배치 작업**
|
||||
- 여러 VM 동시 시작/정지
|
||||
- 약국별 전체 VM 관리
|
||||
- 예약 작업 (특정 시간에 VM 시작/정지)
|
||||
|
||||
### 3. 동적 VNC 접속 시스템
|
||||
- **호스트 자동 선택**
|
||||
```python
|
||||
def connect_to_vm(pharmacy_id, vmid):
|
||||
# 1. pharmacy_id로 Proxmox 호스트 정보 조회
|
||||
host_info = get_proxmox_host_by_pharmacy(pharmacy_id)
|
||||
|
||||
# 2. 해당 호스트에서 VM 정보 확인
|
||||
vm_info = get_vm_info(host_info, vmid)
|
||||
|
||||
# 3. VNC 티켓 생성 및 연결
|
||||
vnc_ticket = create_vnc_ticket(host_info, vm_info)
|
||||
return vnc_ticket
|
||||
```
|
||||
|
||||
- **다중 세션 관리**
|
||||
- 서로 다른 Proxmox 호스트의 여러 VM에 동시 VNC 접속
|
||||
- 세션별 독립적인 티켓 관리
|
||||
- 탭 기반 다중 VNC 창 지원
|
||||
|
||||
### 4. 모니터링 및 알림
|
||||
- **리소스 모니터링**
|
||||
- 모든 Proxmox 호스트의 CPU, 메모리, 스토리지 사용량
|
||||
- VM별 리소스 사용 현황
|
||||
- 임계치 초과 시 알림
|
||||
|
||||
- **이벤트 로그**
|
||||
- VM 시작/정지 이력
|
||||
- VNC 접속 이력
|
||||
- 시스템 오류 및 복구 이력
|
||||
|
||||
## 🛠️ API 설계
|
||||
|
||||
### Proxmox 호스트 관리 API
|
||||
```http
|
||||
GET /api/proxmox/hosts # 호스트 목록 조회
|
||||
POST /api/proxmox/hosts # 호스트 등록
|
||||
PUT /api/proxmox/hosts/{host_id} # 호스트 정보 수정
|
||||
DELETE /api/proxmox/hosts/{host_id} # 호스트 삭제
|
||||
GET /api/proxmox/hosts/{host_id}/health # 호스트 상태 확인
|
||||
```
|
||||
|
||||
### 통합 VM 관리 API
|
||||
```http
|
||||
GET /api/vms # 모든 호스트의 VM 목록
|
||||
GET /api/vms/pharmacy/{pharmacy_id} # 특정 약국의 VM 목록
|
||||
GET /api/vms/sync # VM 정보 동기화
|
||||
POST /api/vms/batch/start # 여러 VM 일괄 시작
|
||||
POST /api/vms/batch/stop # 여러 VM 일괄 정지
|
||||
```
|
||||
|
||||
### 동적 VNC 접속 API
|
||||
```http
|
||||
POST /api/vnc/connect # 동적 VNC 연결 생성
|
||||
# Request Body:
|
||||
{
|
||||
"pharmacy_id": 1,
|
||||
"vmid": 101,
|
||||
"vm_type": "kiosk"
|
||||
}
|
||||
|
||||
GET /api/vnc/sessions # 활성 VNC 세션 목록
|
||||
POST /api/vnc/refresh/{session_id} # VNC 티켓 새로고침 (기존)
|
||||
```
|
||||
|
||||
## 🎨 UI/UX 설계
|
||||
|
||||
### 1. 대시보드 개선
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 🏥 PharmQ - 다중 Proxmox 관리 시스템 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 📊 전체 현황 │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │호스트수 │ │총VM수 │ │실행중 │ │오프라인 │ │
|
||||
│ │ 100 │ │ 450 │ │ 380 │ │ 15 │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 🔍 검색 및 필터 │
|
||||
│ [약국명/IP 검색____] [호스트상태▼] [VM타입▼] [VM상태▼] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. 호스트 목록 페이지
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 🖥️ Proxmox 호스트 관리 [+ 호스트 추가] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 약국명 │ IP주소 │ 상태 │ VM수 │ 마지막확인 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 🟢 약국A │ 100.64.0.10 │ 온라인 │ 5개 │ 1분전 │
|
||||
│ 🔴 약국B │ 100.64.0.11 │ 오프라인│ - │ 30분전 │
|
||||
│ 🟢 약국C │ 100.64.0.12 │ 온라인 │ 3개 │ 2분전 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. 통합 VM 관리 페이지
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 🔧 통합 VM 관리 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 📍 약국A (100.64.0.10) - 온라인 │
|
||||
│ ├── 🖥️ VM101: Kiosk-1 [실행중] [VNC] [정지] │
|
||||
│ ├── 🖥️ VM102: POS-System [정지됨] [시작] [VNC] │
|
||||
│ └── 🖥️ VM103: Backup [실행중] [VNC] [정지] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 📍 약국C (100.64.0.12) - 온라인 │
|
||||
│ ├── 🖥️ VM201: Server [실행중] [VNC] [정지] │
|
||||
│ └── 🖥️ VM202: Kiosk [실행중] [VNC] [정지] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔧 구현 단계
|
||||
|
||||
### Phase 1: 기반 시스템 확장 (1-2주)
|
||||
1. **데이터베이스 스키마 확장**
|
||||
- 다중 호스트 테이블 생성
|
||||
- 기존 데이터 마이그레이션
|
||||
|
||||
2. **Proxmox 클라이언트 다중화**
|
||||
- 호스트별 클라이언트 풀 관리
|
||||
- 연결 관리 및 로드밸런싱
|
||||
|
||||
### Phase 2: 호스트 관리 시스템 (2-3주)
|
||||
1. **호스트 등록/관리 기능**
|
||||
- CRUD API 구현
|
||||
- 관리 UI 개발
|
||||
|
||||
2. **상태 모니터링**
|
||||
- 헬스체크 시스템
|
||||
- 알림 기능
|
||||
|
||||
### Phase 3: 통합 VM 관리 (2-3주)
|
||||
1. **다중 호스트 VM 조회**
|
||||
- 통합 대시보드
|
||||
- 검색/필터링 기능
|
||||
|
||||
2. **배치 작업 시스템**
|
||||
- 일괄 작업 API
|
||||
- 스케줄링 기능
|
||||
|
||||
### Phase 4: 고도화 기능 (2-3주)
|
||||
1. **성능 최적화**
|
||||
- 캐싱 시스템
|
||||
- 비동기 처리
|
||||
|
||||
2. **보안 강화**
|
||||
- 접근 권한 관리
|
||||
- 감사 로그
|
||||
|
||||
## 🚨 고려사항
|
||||
|
||||
### 기술적 도전과제
|
||||
1. **네트워크 지연**
|
||||
- Headscale VPN을 통한 다중 호스트 접근 시 지연 가능성
|
||||
- 연결 타임아웃 및 재시도 로직 필요
|
||||
|
||||
2. **확장성**
|
||||
- 100개 호스트 동시 관리 시 리소스 사용량
|
||||
- 동시 VNC 세션 수 제한 고려
|
||||
|
||||
3. **장애 처리**
|
||||
- 개별 호스트 오프라인 시 graceful degradation
|
||||
- 부분 장애 상황에서의 서비스 연속성
|
||||
|
||||
### 보안 고려사항
|
||||
1. **인증 정보 관리**
|
||||
- 각 Proxmox 호스트별 패스워드 암호화 저장
|
||||
- API 토큰 순환 정책
|
||||
|
||||
2. **네트워크 보안**
|
||||
- Headscale 네트워크 내부 통신 암호화
|
||||
- VNC 세션 보안 강화
|
||||
|
||||
## 📈 성공 지표
|
||||
|
||||
### 기능적 지표
|
||||
- ✅ 100개 Proxmox 호스트 동시 관리
|
||||
- ✅ 1000개 이상 VM 통합 관리
|
||||
- ✅ 동시 VNC 세션 50개 이상 지원
|
||||
- ✅ 호스트 상태 확인 응답 시간 < 5초
|
||||
|
||||
### 성능 지표
|
||||
- ✅ 대시보드 로딩 시간 < 3초
|
||||
- ✅ VM 목록 조회 시간 < 5초
|
||||
- ✅ VNC 연결 설정 시간 < 10초
|
||||
- ✅ 시스템 가용성 99.5% 이상
|
||||
|
||||
## 🎯 결론
|
||||
|
||||
이 다중 Proxmox 호스트 관리 시스템을 통해 약국별로 분산된 IT 인프라를 중앙에서 효율적으로 관리할 수 있게 됩니다. Headscale 네트워크를 활용한 안전한 연결과 통합 관리 인터페이스를 제공하여 운영 효율성을 크게 향상시킬 수 있을 것입니다.
|
||||
|
||||
---
|
||||
|
||||
*이 기획서는 현재 단일 Proxmox 호스트 관리 시스템을 다중 호스트 환경으로 확장하기 위한 상세 계획을 담고 있습니다. 단계적 구현을 통해 안정적이고 확장 가능한 시스템을 구축할 수 있을 것입니다.*
|
||||
230
VNC-WebSocket-Proxy-Solution.md
Normal file
230
VNC-WebSocket-Proxy-Solution.md
Normal file
@ -0,0 +1,230 @@
|
||||
# VNC WebSocket 연결 문제 및 프록시 솔루션
|
||||
|
||||
## 🚨 현재 문제 상황
|
||||
|
||||
### WebSocket 1006 연결 실패 원인
|
||||
```
|
||||
WebSocket connection to 'wss://pve7.0bin.in/api2/json/nodes/pve7/qemu/102/vncwebsocket...' failed:
|
||||
Failed when connecting: Connection closed (code: 1006)
|
||||
```
|
||||
|
||||
**WebSocket Error Code 1006**는 "Abnormal Closure"를 의미하며, 연결이 설정되기도 전에 종료되었음을 나타냅니다.
|
||||
|
||||
## 🔍 근본 원인 분석
|
||||
|
||||
### 1. 브라우저 보안 정책 (Mixed Content)
|
||||
- **현재 상황**: HTTPS 웹사이트 (`pqadmin.0bin.in`)에서 자체 서명 인증서를 사용하는 WebSocket 연결 시도
|
||||
- **브라우저 제한**: Chrome, Firefox 등 모던 브라우저는 HTTPS 페이지에서 신뢰할 수 없는 SSL 인증서로의 WebSocket 연결을 자동 차단
|
||||
- **에러 발생**: 브라우저가 연결 시도 자체를 거부하여 1006 에러 발생
|
||||
|
||||
### 2. SSL/TLS 인증서 신뢰 문제
|
||||
```bash
|
||||
# Proxmox 서버의 자체 서명 인증서
|
||||
Subject: CN=Proxmox Virtual Environment
|
||||
Issuer: PVE Cluster Manager CA
|
||||
```
|
||||
- **자체 서명 인증서**: `pve7.0bin.in`이 공인 CA가 아닌 자체 서명 인증서 사용
|
||||
- **브라우저 불신**: 사용자가 명시적으로 인증서를 신뢰하지 않으면 WebSocket 연결 거부
|
||||
|
||||
### 3. CORS (Cross-Origin Resource Sharing) 정책
|
||||
- **Origin 불일치**: `pqadmin.0bin.in`에서 `pve7.0bin.in`으로의 크로스 오리진 WebSocket 연결
|
||||
- **Preflight 검사**: 브라우저가 연결 전 OPTIONS 요청을 보내지만 Proxmox가 적절한 CORS 헤더를 반환하지 않을 가능성
|
||||
|
||||
## 💡 해결책 1: 역방향 프록시 (Reverse Proxy)
|
||||
|
||||
### 구현 방식: nginx를 통한 WebSocket 프록시
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/pqadmin-vnc
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name pqadmin.0bin.in;
|
||||
|
||||
ssl_certificate /path/to/ssl/cert.pem;
|
||||
ssl_certificate_key /path/to/ssl/key.pem;
|
||||
|
||||
# VNC WebSocket 프록시
|
||||
location /vnc-proxy/ {
|
||||
# WebSocket 업그레이드 헤더
|
||||
proxy_pass https://pve7.0bin.in/api2/json/nodes/pve7/qemu/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $proxy_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;
|
||||
|
||||
# SSL 검증 비활성화 (자체 서명 인증서 때문)
|
||||
proxy_ssl_verify off;
|
||||
proxy_ssl_server_name on;
|
||||
|
||||
# 타임아웃 설정
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 장점
|
||||
- ✅ **같은 도메인**: `pqladmin.0bin.in/vnc-proxy/`로 WebSocket 연결하여 CORS 문제 해결
|
||||
- ✅ **신뢰할 수 있는 SSL**: nginx가 공인 인증서를 사용하여 브라우저 신뢰 확보
|
||||
- ✅ **투명한 프록시**: 클라이언트는 Proxmox 직접 연결과 동일하게 작동
|
||||
|
||||
### 구현 단계
|
||||
```bash
|
||||
# 1. nginx 설정 파일 생성
|
||||
sudo nano /etc/nginx/sites-available/pqadmin-vnc
|
||||
|
||||
# 2. 심볼릭 링크 생성
|
||||
sudo ln -s /etc/nginx/sites-available/pqadmin-vnc /etc/nginx/sites-enabled/
|
||||
|
||||
# 3. nginx 설정 테스트
|
||||
sudo nginx -t
|
||||
|
||||
# 4. nginx 재시작
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 클라이언트 코드 수정
|
||||
```javascript
|
||||
// 기존: 직접 Proxmox 연결
|
||||
// const websocketUrl = 'wss://pve7.0bin.in/api2/json/nodes/pve7/qemu/102/vncwebsocket?...'
|
||||
|
||||
// 변경: nginx 프록시를 통한 연결
|
||||
const websocketUrl = 'wss://pqladmin.0bin.in/vnc-proxy/102/vncwebsocket?...'
|
||||
```
|
||||
|
||||
## 💡 해결책 2: WebSocket 프록시 서버
|
||||
|
||||
### 구현 방식: Node.js/Python WebSocket 중계 서버
|
||||
|
||||
```python
|
||||
# vnc_websocket_proxy.py
|
||||
import asyncio
|
||||
import websockets
|
||||
import ssl
|
||||
import json
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
class VNCWebSocketProxy:
|
||||
def __init__(self):
|
||||
self.proxmox_host = "pve7.0bin.in"
|
||||
self.proxmox_port = 443
|
||||
|
||||
async def proxy_websocket(self, websocket, path):
|
||||
# 클라이언트 요청 파싱
|
||||
query_params = parse_qs(path.split('?', 1)[1] if '?' in path else '')
|
||||
vmid = query_params.get('vmid', [''])[0]
|
||||
ticket = query_params.get('vncticket', [''])[0]
|
||||
|
||||
# Proxmox WebSocket URL 생성
|
||||
proxmox_url = f"wss://{self.proxmox_host}:{self.proxmox_port}/api2/json/nodes/pve7/qemu/{vmid}/vncwebsocket?port=5900&vncticket={ticket}"
|
||||
|
||||
# SSL 컨텍스트 (자체 서명 인증서 허용)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
# Proxmox WebSocket 연결
|
||||
async with websockets.connect(proxmox_url, ssl=ssl_context) as proxmox_ws:
|
||||
# 양방향 데이터 중계
|
||||
await asyncio.gather(
|
||||
self.forward_messages(websocket, proxmox_ws),
|
||||
self.forward_messages(proxmox_ws, websocket)
|
||||
)
|
||||
except Exception as e:
|
||||
await websocket.send(json.dumps({"error": f"Proxmox connection failed: {str(e)}"}))
|
||||
|
||||
async def forward_messages(self, source, destination):
|
||||
try:
|
||||
async for message in source:
|
||||
await destination.send(message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
|
||||
# 프록시 서버 시작
|
||||
if __name__ == "__main__":
|
||||
proxy = VNCWebSocketProxy()
|
||||
start_server = websockets.serve(proxy.proxy_websocket, "0.0.0.0", 8765)
|
||||
asyncio.get_event_loop().run_until_complete(start_server)
|
||||
asyncio.get_event_loop().run_forever()
|
||||
```
|
||||
|
||||
### 장점
|
||||
- ✅ **완전한 제어**: WebSocket 연결 과정을 완전히 제어 가능
|
||||
- ✅ **에러 처리**: 연결 실패 시 상세한 에러 메시지 제공
|
||||
- ✅ **로깅**: 모든 WebSocket 트래픽 로깅 및 디버깅 가능
|
||||
- ✅ **인증 확장**: 추가적인 인증 로직 구현 가능
|
||||
|
||||
### 구현 단계
|
||||
```bash
|
||||
# 1. 의존성 설치
|
||||
pip install websockets asyncio
|
||||
|
||||
# 2. 프록시 서버 실행
|
||||
python vnc_websocket_proxy.py
|
||||
|
||||
# 3. systemd 서비스 등록
|
||||
sudo systemctl enable vnc-websocket-proxy.service
|
||||
```
|
||||
|
||||
### 클라이언트 코드 수정
|
||||
```javascript
|
||||
// WebSocket 프록시 서버로 연결
|
||||
const websocketUrl = 'ws://localhost:8765/vnc-proxy?vmid=102&vncticket=' + encodeURIComponent(ticket);
|
||||
```
|
||||
|
||||
## 📊 솔루션 비교
|
||||
|
||||
| 기준 | 역방향 프록시 (nginx) | WebSocket 프록시 서버 |
|
||||
|------|----------------------|---------------------|
|
||||
| **구현 복잡도** | 🟡 중간 | 🔴 높음 |
|
||||
| **성능** | 🟢 우수 | 🟡 양호 |
|
||||
| **유지보수** | 🟢 쉬움 | 🟡 보통 |
|
||||
| **확장성** | 🟡 제한적 | 🟢 우수 |
|
||||
| **디버깅** | 🟡 제한적 | 🟢 우수 |
|
||||
| **보안** | 🟢 안전 | 🟡 주의 필요 |
|
||||
| **리소스 사용량** | 🟢 낮음 | 🟡 보통 |
|
||||
|
||||
## 🎯 추천 솔루션
|
||||
|
||||
### 단기 해결책: 역방향 프록시 (nginx)
|
||||
1. **빠른 구현**: 기존 nginx 설정에 location 블록만 추가
|
||||
2. **안정성**: nginx의 검증된 WebSocket 프록시 기능 활용
|
||||
3. **보안**: 공인 SSL 인증서로 브라우저 신뢰 확보
|
||||
|
||||
### 장기 해결책: 하이브리드 접근
|
||||
1. **nginx 프록시**: 기본적인 WebSocket 중계
|
||||
2. **커스텀 미들웨어**: 인증, 로깅, 모니터링 기능 추가
|
||||
3. **로드 밸런싱**: 다중 Proxmox 호스트 지원
|
||||
|
||||
## 🚀 구현 우선순위
|
||||
|
||||
### Phase 1: nginx 역방향 프록시 구현
|
||||
```bash
|
||||
# 1. nginx 설정 파일 작성
|
||||
# 2. SSL 인증서 설정
|
||||
# 3. WebSocket 프록시 규칙 추가
|
||||
# 4. 클라이언트 URL 변경
|
||||
```
|
||||
|
||||
### Phase 2: 에러 처리 및 모니터링 강화
|
||||
```bash
|
||||
# 1. 연결 실패 시 재시도 로직
|
||||
# 2. WebSocket 상태 모니터링
|
||||
# 3. 로그 수집 및 분석
|
||||
```
|
||||
|
||||
### Phase 3: 다중 호스트 지원
|
||||
```bash
|
||||
# 1. 동적 백엔드 라우팅
|
||||
# 2. 헬스체크 구현
|
||||
# 3. 로드 밸런싱 설정
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**결론**: WebSocket 1006 에러는 브라우저 보안 정책과 인증서 신뢰 문제가 주요 원인입니다. nginx 역방향 프록시를 통해 이 문제를 근본적으로 해결할 수 있으며, 이는 가장 실용적이고 안정적인 솔루션입니다.
|
||||
@ -581,11 +581,7 @@ def create_app(config_name=None):
|
||||
print(f"🔄 VNC 티켓 새로고침 요청: {node}/{vmid}")
|
||||
|
||||
# Proxmox 클라이언트 생성
|
||||
client = ProxmoxClient(
|
||||
host=config['proxmox']['host'],
|
||||
username=config['proxmox']['username'],
|
||||
password=config['proxmox']['password']
|
||||
)
|
||||
client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD)
|
||||
|
||||
if not client.login():
|
||||
return jsonify({'error': 'Proxmox 로그인 실패'}), 500
|
||||
|
||||
@ -74,10 +74,31 @@
|
||||
|
||||
// This function is called when we are disconnected
|
||||
function disconnectedFromServer(e) {
|
||||
console.log('🔌 VNC 연결 해제 상세 정보:', e.detail);
|
||||
|
||||
if (e.detail.clean) {
|
||||
status("Disconnected");
|
||||
status("연결이 정상적으로 종료되었습니다");
|
||||
} else {
|
||||
status("Something went wrong, connection is closed");
|
||||
const reason = e.detail.reason || 'Unknown';
|
||||
status(`연결 실패: ${reason} (Code: ${e.detail.code || 'Unknown'})`);
|
||||
console.error('❌ VNC 연결 실패 상세:', {
|
||||
code: e.detail.code,
|
||||
reason: e.detail.reason,
|
||||
wasClean: e.detail.clean
|
||||
});
|
||||
|
||||
// WebSocket 에러 코드별 메시지
|
||||
const errorMessages = {
|
||||
1006: 'WebSocket 서버에 연결할 수 없습니다. VM이 실행중인지 확인하세요.',
|
||||
1000: '정상적으로 연결이 종료되었습니다.',
|
||||
1002: '프로토콜 오류가 발생했습니다.',
|
||||
1003: '지원하지 않는 데이터를 받았습니다.',
|
||||
1009: '메시지가 너무 큽니다.',
|
||||
1011: '서버에서 예상치 못한 오류가 발생했습니다.'
|
||||
};
|
||||
|
||||
const userFriendlyMessage = errorMessages[e.detail.code] || `알 수 없는 오류 (코드: ${e.detail.code})`;
|
||||
status(`❌ ${userFriendlyMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,22 +211,103 @@
|
||||
console.log('WebSocket URL:', websocketUrl);
|
||||
console.log('VNC Password:', vncPassword);
|
||||
|
||||
status("Connecting");
|
||||
// WebSocket URL 유효성 검사
|
||||
function validateWebSocketUrl(url) {
|
||||
if (!url || typeof url !== 'string') {
|
||||
console.error('❌ WebSocket URL이 비어있습니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Creating a new RFB object will start a new connection
|
||||
rfb = new RFB(document.getElementById('screen'), websocketUrl,
|
||||
{ credentials: { password: vncPassword } });
|
||||
if (!url.startsWith('wss://') && !url.startsWith('ws://')) {
|
||||
console.error('❌ 올바르지 않은 WebSocket URL 프로토콜:', url);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add listeners to important events from the RFB module
|
||||
rfb.addEventListener("connect", connectedToServer);
|
||||
rfb.addEventListener("disconnect", disconnectedFromServer);
|
||||
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
|
||||
rfb.addEventListener("desktopname", updateDesktopName);
|
||||
rfb.addEventListener("securityfailure", onSecurityFailure);
|
||||
console.log('✅ WebSocket URL 유효성 검사 통과');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set parameters that can be changed on an active connection
|
||||
rfb.viewOnly = false;
|
||||
rfb.scaleViewport = true;
|
||||
// VNC 연결 함수
|
||||
function connectToVNC() {
|
||||
if (!validateWebSocketUrl(websocketUrl)) {
|
||||
status("❌ 잘못된 WebSocket URL입니다");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vncPassword || vncPassword.trim() === '') {
|
||||
status("❌ VNC 패스워드가 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
status("Connecting to VM...");
|
||||
console.log('🔄 VNC 연결 시도 시작...');
|
||||
|
||||
try {
|
||||
// WebSocket 연결 직접 테스트
|
||||
console.log('🧪 WebSocket 연결 직접 테스트...');
|
||||
const testWS = new WebSocket(websocketUrl);
|
||||
|
||||
testWS.onopen = function(event) {
|
||||
console.log('✅ WebSocket 연결 테스트 성공');
|
||||
testWS.close();
|
||||
|
||||
// WebSocket이 연결되면 RFB 객체 생성
|
||||
createRFBConnection();
|
||||
};
|
||||
|
||||
testWS.onerror = function(error) {
|
||||
console.error('❌ WebSocket 연결 테스트 실패:', error);
|
||||
status('❌ WebSocket 서버에 연결할 수 없습니다');
|
||||
|
||||
// 대안: 티켓 새로고침 시도
|
||||
setTimeout(() => {
|
||||
status('🔄 새 티켓으로 재시도 중...');
|
||||
refreshVNCTicket();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
testWS.onclose = function(event) {
|
||||
console.log('🔌 WebSocket 테스트 연결 종료:', event.code, event.reason);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ WebSocket 테스트 실패:', error);
|
||||
status(`❌ 연결 초기화 실패: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// RFB 연결 생성 함수
|
||||
function createRFBConnection() {
|
||||
try {
|
||||
console.log('🔄 RFB 객체 생성 시작...');
|
||||
|
||||
// Creating a new RFB object will start a new connection
|
||||
rfb = new RFB(document.getElementById('screen'), websocketUrl,
|
||||
{ credentials: { password: vncPassword } });
|
||||
|
||||
console.log('✅ RFB 객체 생성 완료');
|
||||
|
||||
// Add listeners to important events from the RFB module
|
||||
rfb.addEventListener("connect", connectedToServer);
|
||||
rfb.addEventListener("disconnect", disconnectedFromServer);
|
||||
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
|
||||
rfb.addEventListener("desktopname", updateDesktopName);
|
||||
rfb.addEventListener("securityfailure", onSecurityFailure);
|
||||
|
||||
// Set parameters that can be changed on an active connection
|
||||
rfb.viewOnly = false;
|
||||
rfb.scaleViewport = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ RFB 객체 생성 실패:', error);
|
||||
status(`❌ VNC 클라이언트 초기화 실패: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 연결 시작
|
||||
connectToVNC();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
||||
@ -106,26 +106,46 @@ class ProxmoxClient:
|
||||
def get_vnc_ticket(self, node: str, vmid: int) -> Optional[Dict]:
|
||||
"""VNC 접속 티켓 생성"""
|
||||
try:
|
||||
# 먼저 VM 상태 확인
|
||||
vm_status = self.get_vm_status(node, vmid)
|
||||
print(f"🔍 VM {vmid} 상태: {vm_status}")
|
||||
|
||||
if vm_status.get('status') != 'running':
|
||||
print(f"❌ VM {vmid}이 실행중이 아닙니다. 현재 상태: {vm_status.get('status', 'unknown')}")
|
||||
return None
|
||||
|
||||
data = {
|
||||
'websocket': '1',
|
||||
'generate-password': '1' # 패스워드 생성 활성화
|
||||
}
|
||||
|
||||
print(f"🔄 VNC 티켓 요청: {self.base_url}/nodes/{node}/qemu/{vmid}/vncproxy")
|
||||
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/nodes/{node}/qemu/{vmid}/vncproxy",
|
||||
data=data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
print(f"📡 VNC 티켓 응답 상태: {response.status_code}")
|
||||
print(f"📄 VNC 티켓 응답 내용: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
vnc_data = response.json()['data']
|
||||
print(f"✅ VNC 티켓 생성 성공: {vnc_data}")
|
||||
|
||||
# WebSocket URL 생성
|
||||
# WebSocket URL 생성 (인증 토큰 포함)
|
||||
encoded_ticket = quote_plus(vnc_data['ticket'])
|
||||
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}"
|
||||
# Proxmox 세션 쿠키도 함께 포함 (CSRFPreventionToken도 필요할 수 있음)
|
||||
csrf_token = getattr(self, 'csrf_token', None)
|
||||
if csrf_token:
|
||||
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}&CSRFPreventionToken={csrf_token}"
|
||||
else:
|
||||
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}"
|
||||
|
||||
# 디버깅 정보 추가
|
||||
print(f"🔗 WebSocket URL: {vnc_data['websocket_url']}")
|
||||
print(f"🔑 VNC Password: {vnc_data.get('password', 'N/A')}")
|
||||
return vnc_data
|
||||
else:
|
||||
print(f"❌ VNC 티켓 생성 HTTP 오류: {response.status_code}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user