|
|
|
|
@@ -657,13 +657,13 @@ async function injectUpnpToRunBat(installPath: string): Promise<void> {
|
|
|
|
|
'for /f "tokens=2 delims==" %%a in (\'findstr /b /c:"server-port=" server.properties 2^>nul\') do set "_MQ_PORT=%%a"',
|
|
|
|
|
'set "_MQ_PORT=%_MQ_PORT: =%"',
|
|
|
|
|
'echo [MusicQuiz] UPnP 등록 시도: TCP %_MQ_PORT%',
|
|
|
|
|
'powershell -NoProfile -Command "$port=[int]$env:_MQ_PORT; $ip=(Get-NetIPAddress -AddressFamily IPv4 -PrefixOrigin Dhcp,Manual -ErrorAction SilentlyContinue ^| Where-Object {$_.IPAddress -notlike \'169.254.*\' -and $_.IPAddress -ne \'127.0.0.1\'} ^| Select-Object -First 1).IPAddress; if (-not $ip) { Write-Host \'[MusicQuiz] 로컬 IPv4 검색 실패\'; exit 1 }; try { $u = New-Object -ComObject HNetCfg.NATUPnP.1; $c=$u.StaticPortMappingCollection; if ($c) { try { $c.Remove($port,\'TCP\') ^| Out-Null } catch {}; $c.Add($port,\'TCP\',$port,$ip,$true,\'MusicQuiz Minecraft Server\') ^| Out-Null; Write-Host (\'[MusicQuiz] UPnP 등록 성공: \' + $ip + \':\' + $port + \' TCP\') } else { Write-Host \'[MusicQuiz] UPnP 컬렉션 사용 불가(라우터 UPnP 꺼짐?)\' } } catch { Write-Host (\'[MusicQuiz] UPnP 등록 실패: \' + $_.Exception.Message) }"'
|
|
|
|
|
'powershell -NoProfile -Command "$port=[int]$env:_MQ_PORT; $ip=(Get-NetIPAddress -AddressFamily IPv4 -PrefixOrigin Dhcp,Manual -ErrorAction SilentlyContinue | Where-Object {$_.IPAddress -notlike \'169.254.*\' -and $_.IPAddress -ne \'127.0.0.1\'} | Select-Object -First 1).IPAddress; if (-not $ip) { Write-Host \'[MusicQuiz] 로컬 IPv4 검색 실패\'; exit 1 }; try { $u = New-Object -ComObject HNetCfg.NATUPnP.1; $c=$u.StaticPortMappingCollection; if ($c) { try { $c.Remove($port,\'TCP\') | Out-Null } catch {}; $c.Add($port,\'TCP\',$port,$ip,$true,\'MusicQuiz Minecraft Server\') | Out-Null; Write-Host (\'[MusicQuiz] UPnP 등록 성공: \' + $ip + \':\' + $port + \' TCP\') } else { Write-Host \'[MusicQuiz] UPnP 컬렉션 사용 불가(라우터 UPnP 꺼짐?)\' } } catch { Write-Host (\'[MusicQuiz] UPnP 등록 실패: \' + $_.Exception.Message) }"'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const removeBlock = [
|
|
|
|
|
'REM 서버 종료 후: UPnP 매핑 해제.',
|
|
|
|
|
'echo [MusicQuiz] UPnP 해제 시도: TCP %_MQ_PORT%',
|
|
|
|
|
'powershell -NoProfile -Command "$port=[int]$env:_MQ_PORT; try { $u = New-Object -ComObject HNetCfg.NATUPnP.1; $c=$u.StaticPortMappingCollection; if ($c) { $c.Remove($port,\'TCP\') ^| Out-Null; Write-Host (\'[MusicQuiz] UPnP 해제 완료: TCP \' + $port) } } catch { Write-Host (\'[MusicQuiz] UPnP 해제 실패: \' + $_.Exception.Message) }"'
|
|
|
|
|
'powershell -NoProfile -Command "$port=[int]$env:_MQ_PORT; try { $u = New-Object -ComObject HNetCfg.NATUPnP.1; $c=$u.StaticPortMappingCollection; if ($c) { $c.Remove($port,\'TCP\') | Out-Null; Write-Host (\'[MusicQuiz] UPnP 해제 완료: TCP \' + $port) } } catch { Write-Host (\'[MusicQuiz] UPnP 해제 실패: \' + $_.Exception.Message) }"'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const merged: string[] = []
|
|
|
|
|
@@ -1063,8 +1063,14 @@ async function probePortFromOutside(
|
|
|
|
|
details.push(t('log.detailIfconfigFail', { error: (externalResult as { error: string }).error }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 임시 리스너가 떴고 외부 서비스도 닿지 않았다면 명확한 false.
|
|
|
|
|
if (reachable === null && listenerBound && !gotInboundConnection) reachable = false
|
|
|
|
|
// '닫힘(false)' 판정은 외부 점검 서비스(ifconfig.co)가 명시적으로 reachable=false 를
|
|
|
|
|
// 돌려준 경우에만 인정한다(위 분기에서 처리). 임시 리스너에 인바운드가 안 왔다는 사실만으로는
|
|
|
|
|
// false 로 단정하지 않는다:
|
|
|
|
|
// - ifconfig.co 가 타임아웃/레이트리밋으로 실패하면 애초에 외부에서 연결을 시도한 적이 없으므로
|
|
|
|
|
// '리스너 미도달'은 아무 의미가 없다(과거엔 이 경우를 false 로 오탐했다).
|
|
|
|
|
// - 인바운드를 받는 주체가 마크 서버가 아니라 설치기(node/electron) 프로세스라, Windows 방화벽이
|
|
|
|
|
// 설치기 인바운드만 막아도 포워딩이 정상이어도 '리스너 미도달'이 된다.
|
|
|
|
|
// 따라서 외부 판정이 없으면 reachable=null(확인 불가)로 남겨 UPnP 시도/안내 단계로 넘긴다.
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
reachable,
|
|
|
|
|
@@ -1073,7 +1079,21 @@ async function probePortFromOutside(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fetchIfconfigCoPort(port: number): Promise<{ ok: true; reachable: boolean | null; ip: string } | { ok: false; error: string }> {
|
|
|
|
|
type IfconfigPortResult = { ok: true; reachable: boolean | null; ip: string } | { ok: false; error: string }
|
|
|
|
|
|
|
|
|
|
// ifconfig.co 는 간헐적으로 타임아웃/레이트리밋(429)을 낸다. 실패를 곧장 '확인 불가'로
|
|
|
|
|
// 넘기기 전에 한 번 더 시도해서 일시적 실패로 인한 오탐/미확인을 줄인다.
|
|
|
|
|
async function fetchIfconfigCoPort(port: number): Promise<IfconfigPortResult> {
|
|
|
|
|
let last: IfconfigPortResult = { ok: false, error: 'no attempt' }
|
|
|
|
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
|
|
|
last = await fetchIfconfigCoPortOnce(port)
|
|
|
|
|
if (last.ok) return last
|
|
|
|
|
if (attempt === 0) await sleep(1500)
|
|
|
|
|
}
|
|
|
|
|
return last
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fetchIfconfigCoPortOnce(port: number): Promise<IfconfigPortResult> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const target = new URL(`https://ifconfig.co/port/${port}`)
|
|
|
|
|
const req = https.get(target, {
|
|
|
|
|
|