Skip to content

feat(connector): [LBT-147] VM 접속 대상 검증과 SSH 연결 관리 - #84

Open
paekseobin-crypto wants to merge 17 commits into
mainfrom
feat/LBT-147-vm-access
Open

paekseobin-crypto wants to merge 17 commits into
mainfrom
feat/LBT-147-vm-access

Conversation

@paekseobin-crypto

@paekseobin-crypto paekseobin-crypto commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

변경 요약

LBT-147의 VM Access 기반 전체를 구현합니다. 인증된 LabInstance/generation/VM key/Provider Server에 맞는 Management SSH를 연결하고, 같은 VM의 PTY/SFTP가 SSH transport를 공유하되 채널 수명은 독립적으로 관리합니다.

  • Provider 소유 metadata·project·현재 generation·논리 VM key·Server 및 Management NIC/private IP를 검증하고, 잘못된 대상과 stale generation을 SSH 전에 거절합니다. 고객 로컬 key와 기존 Server별 host-key pin을 재사용합니다.
  • VM별 lazy shared SSH manager와 독립 PTY/SFTP 채널을 production app/Terminal에 연결합니다. 마지막 채널 종료 뒤 runtime 0.1.7의 1m idle timeout으로 transport를 정리합니다.
  • target 교체, Reset/Cleanup 부분 실패, 인증·접속·handshake/setup timeout/cancel, disconnect/shutdown과 지연된 이전 generation 요청을 처리합니다. raw Provider/SSH 오류·개인키·VM 주소를 Control/Data 오류에 노출하지 않고 Terminal/SFTP 내용을 저장하지 않습니다.
  • 최신 main d78296f를 feature branch에 통합했습니다. main 자체를 push/merge하지 않았습니다.

리뷰 및 추가 감사 수정

CODEOWNER review와 전체 pre-merge audit에서 확인한 문제를 모두 수정했습니다.

Finding 변경과 회귀
R1 / P1: B open/setup 취소가 A의 공유 SSH 종료 취소는 B 채널만 해제합니다. transport당 context 없는 SSH 작업1개와 cleanup worker1개로 제한하고 late result를 소유해 닫습니다. active A + B open/PTY/SFTP timeout/cancel6분기, late accept/reuse,24 canceled waiters, close-ACK 무응답 fixture를 검증했습니다.
R2 / P2: Provider 운영 장애를 INVALID_SESSION으로 분류 명시적 target/ownership/stale/server404만 INVALID_SESSION입니다. Nova/Neutron503/401·malformed·설정·context·BUILD/no IP는 safe ATTACH_FAILED입니다. Adapter→Manager→Control17분기 및 redaction을 검증했습니다.
R3 / P2: shutdown 순서 때문에 reason 경쟁·ENDED 누락 Terminal admission 차단 → SERVICE_RESTARTING 종료/ENDED → VMAccess close → Control close 순서로 고정했습니다. ordinary reconnect에는 shutdown hook을 실행하지 않습니다. wire/enum/schema 변경은 없습니다.
Data backpressure / P1·P2 unread Data TCP write가 writeMu를 보유해 shutdown·connection replace/detach를 막았습니다. active write는 socket Close로 끊고, final Data ENDED는 writer lock을 최대50ms 기다린 뒤 기존 session당1초 deadline으로 씁니다.50ms 초과에서는 best effort close로 계속 정리합니다. replace/detach 뒤 PTY 생존·새 peer output·grace resume와 brief-writer ENDED red/green을 검증했습니다.
SSH Close/Resize backpressure / P1 unread SSH peer에서 stdin/session Close와 WindowChange가 network write를 기다리며 shutdown을 막고 Write/Close race를 만들었습니다. managed PTY Close는 shared manager에 channel release만 위임하고, standalone은 own client socket을 먼저 닫습니다. Resize는 순서를 직렬화하되 state mutex 밖에서 network 호출합니다. actual VMAccessManager→Terminal shutdown, worker release, backlog drain 뒤 server EOF까지 회귀를 추가했습니다.
tracing runtime test / P2(test) timeout context는 elapsed 측정보다 먼저 시작하므로 elapsed >= budget 하한이 정상 deadline을 false fail했습니다. production runtime timeout은 변경하지 않고 context.DeadlineExceeded, 기존 상한, export_timeout 진단을 검증하도록 test assertion만 수정했습니다.
Linux manager fixture / P1(test/CI) run38226445266은 특정 internal/poll.(*FD).Write stack을 요구한 OS 의존 assertion, run38229011898은 server EOF fixture, run38229988462은 남은 EOF fixture를 검출했습니다. runtime stack 문자열 의존과 server의 인위적1KiB receive buffer를 제거하고, 실제 read pause·200ms pending input/resize·750ms shutdown·worker release·backlog drain/EOF·ErrClosed 계약은 유지했습니다.
Linux healthy Data ENDED / P1(product/CI) run38229988462은 직전 echo writer와 잠깐 겹친 단일 immediate TryLock 때문에 healthy Data ENDED가 누락되는 race도 검출했습니다. 최대50ms bounded lock wait와 결정적 red/green 회귀를 추가했습니다. 독립 normal/race 각180 PASS, app100회 PASS,20 busy sessions 약1005ms·callback20·registry0을 확인했습니다.
Evidence publication / P1·P2·P3 source EOL 집계, Git clean filter, 고정 scratch, CRLF 오판을 수정한 뒤 원격 Go CI가 게시 _fixtures 2개의 gofmt 누락을 검출해 format 단계에서 실패했습니다. 수정 전 파일과 run/job·해시를 보존하고 fixture를 gofmt만 적용했으며, 실행 source를 current로 과대표기하던 문구도 historical snapshot과 e838 검증으로 분리했습니다. 전체 Go 파일 출력0, overlay compile/cloud0, package21, checksum20, ZIP405 CRC/SHA를 재검증했습니다.

최신 main 통합 및 현재 HEAD 재검증 (2026-10-13)

현재 HEAD는 cf19feee51f8cfeaf4557fa11063f0be1e41ddbd입니다.

  • 최신 main d78296f의 LBT-158 KT D1 조회 코드와 LBT-147의 LabInstance/generation/VM/Server target affinity 검증을 함께 유지했습니다. 일반 OpenStack은 기존 Management port 경로, KT D1 설정은 명시적 non-shared ACTIVE Management Tier 경로를 사용합니다.
  • Control 수신 테스트는 연결 소유자인 Supervisor가 shutdown callback 뒤 socket을 닫는 실제 순서를 반영했습니다. TERMINAL_ENDED(SERVICE_RESTARTING) 전송 전에 Control socket을 닫는 시도는 상위 회귀가 검출하여 제거했습니다.
  • SSH backpressure 회귀의 고정 128 KiB goroutine stack snapshot이 반복 suite에서 잘리는 문제를 수정했습니다. 제품 종료 코드는 추가 변경하지 않았습니다.
  • 로컬 자동 검증: go vet ./..., go test ./..., go build ./..., 관련 6개 패키지 race 10회, WSS 종료 100회, 앱 종료 10회, SSH backpressure 50회 PASS. Web typecheck/lint/build 및 13파일 163테스트, OpenAPI·JSON Schema 8개·runtime·migration 7개 PASS.
  • 현재 HEAD로 로컬 OpenStack labbit-test acceptance PASS: gen1 Provision/SSH/PTTY/SFTP/shared transport/idle retirement, gen2 Reset/new target/stale gen1 거절, 추적 리소스 14개 cleanup, 사후 discovery servers=0.
  • KTCloud에는 접속·Provision·Reset·Cleanup을 실행하지 않았습니다. KTCloud 실환경 검증은 LBT-151 범위입니다.
  • 독립 reviewer의 최종 diff 및 race 재검토에서 새 confirmed product blocker는 없었습니다. 새 HEAD의 원격 Linux format/vet/unit/build/PostgreSQL16, Web, Contracts, Backend Image CI가 모두 PASS했습니다. CODEOWNER 재리뷰는 merge gate로 남습니다.

관련 Jira

  • Jira: LBT-147
  • 상위 LBT-21, 선행 LBT-145
  • 후속: LBT-148 PTY/CC02, LBT-149 SFTP, LBT-150 File WSS, C2 LBT-22/26/27

최종 검증

최신 Jira LBT-147와 D-18 v5, D-20 v7, D-21 v10, 테스트 전략 v3, MVP v19를 다시 확인했습니다. 전체 PR diff와 consumer, 실패·보안·수명주기, 이전 Evidence까지 독립 reviewer가 별도로 검토했습니다.

실행 결과·범위
전체 Go counted finalGoSource(ce02+working manager test)에서 fmt/mod download/vet/unit/build PASS.2885 test/subtest PASS,fail0,7SKIP(cloud opt-in6 + Windows POSIX0600 assertion1). 이후 e838 delta는 별도 전체 local Go·targeted/race·Linux CI로 검증
관련6패키지 race VMAccess/Provider/App/Terminal/WSS/tracing482 PASS,fail0,6 cloud opt-in SKIP
핵심 회귀 race20회 940 test/subtest records PASS,fail0,skip0.20회 반복 기록이며940개 독립 기능이라는 뜻은 아닙니다.
Web fresh npm11 npm ci, typecheck/lint/test/build PASS.13파일163tests. package-lock 변경 없음
Contracts OpenAPI,8JSONSchema,runtime/migration metadata,Reset schema11 PASS
코드 commit 원격 CI e8384cb: Linux Go run38231218638의 format/vet/unit/build/PostgreSQL16 integration 전부 PASS. Web38231218734, Contracts38231218756, backend image build38231218620 PASS
최종 Evidence HEAD 원격 CI 15b2c30: Go run38233543891 format/vet/unit/build/PostgreSQL16, Web38233544015, Contracts38233543924, backend image build38233543929 전부 PASS
로컬 OpenStack 누락4suite source da6b670: M2 Provision,M3 Lifecycle,Control WSS full lifecycle,Internet ON/OFF 전부 PASS. 실행별 exact-ID cleanup assertion + 사후 inventory matching0. 이후 Provider/cloud4 production logic은 불변이지만 최신 source 실행으로 바꿔 부르지 않습니다.
정상 실제 VM snapshot code bd8071d, race PASS214.69s. authenticated/pinned shared SSH, strict target, PTY IO/resize, PTY close 뒤 같은 shared SSH SFTP INIT/VERSION/REALPATH, idle 전후 SSH_CONNECTION identity 변화, gen2 Reset/new target/stale1 거절. tracked14 exact-ID absence + final matching0
실제2VM/fault snapshot actual2VMSource의 a5b2d12+working production/test source에서12 parent/subtest PASS + child runControl healthy/unread3PASS. 실제 VM별30 open/exec/close, healthy A와 B exec cancel/deadline 각10, scoped Nova/Neutron503, affinity/404, hostile signer 거절/정상 pin 복구. tracked10 및 보조SG/keypair 정리 + final matching0. 이후 e838의50ms Data wait는 실제 cloud 미재실행이며 loopback/current Linux로 검증
독립 reviewer Data/SSH red→green, actual TCP/WSS/SSH manager, portable current race3, 전체84 committed files 및 실행 source mapping 검토. 후속 e838 변경4파일 Git blob 일치, normal/race 각180, 이전 TryLock 예상실패,20session 누적 대기 검토. 미해결 confirmed product defect 없음
Evidence publication source39 Git blobs, 전체 Go 파일 gofmt -l 출력0, ZIP405 entries CRC/entry SHA, artifact checksums20, known local secret/key+PEM/Bearer scoped scan0, 재현 준비 source/fixture/output guard 및 opt-out overlay compile cloud0 PASS. 원격 format/stack/EOF/Data ENDED 실패와 수정 전 source를 모두 보존

초기 실패도 원본에 보존했습니다. 실제 fixture 첫 실행의 child cwd/readiness budget 문제, 최신 main tracing false failure, CRLF gofmt 진단, 수정 전 Data/SSH red와 첫 candidate fixture 오류, 원격 run38224917705/38226445266/38229011898/38229988462를 최종 PASS로 덮어쓰지 않았습니다.

Evidence: 요약, 전체 audit·AC 추적, 전체 파일 검토, 독립 코드 검토, 독립 게시 검토, 실행 요약, 게시 검증.

남은 미검증 범위와 제약

  • KTCloud 실환경 검증은 이 PR에서 실행하지 않았으며 LBT-151 범위입니다. 현재 HEAD의 로컬 OpenStack PASS를 KTCloud acceptance로 확대하지 않습니다.
  • 실제 Browser↔배포 SaaS↔Connector↔VM CC02, public routing/reconnect/Live는 LBT-148 및 C2 후속입니다.
  • production SFTP/File WSS CRUD는 LBT-149/150 후속입니다. 이번 최신 실제 SFTP 범위는 INIT/VERSION/REALPATH 및 PTY 종료 뒤 채널 독립성입니다.
  • 실제 VM의 unacknowledged pty-req/close-ACK packet fault, 실제 key rotation/MITM, 자연 provider outage, packet loss, hard process restart, 장시간 soak/대규모 load는 미검증입니다.2VM×30은 짧은 동시 부하입니다.
  • actual VM runControl child의 server-side SSH EOF는 독립 관측하지 않았습니다. 별도 actual TCP/production VMAccessManager fixture에서는 EOF를 관측했습니다.
  • 응답하지 않는 contextless SSH open/setup은 transport당1개로 제한하고 기존 active channel은 유지합니다. peer reply/idle/invalidate/manager close 전까지 같은 VM의 새 open/setup은 대기/timeout할 수 있습니다.
  • Data ENDED는 writer contention50ms 초과와 unread peer에서 best effort입니다. 순차 close는 약 N×50ms 경합 대기 뒤 기존 session별1초 frame 기회와 callback 비용이 추가될 수 있으며 process-global shutdown budget은 없습니다. 단절된 Control/Data peer의 전달도 보장하지 않습니다.
  • PostgreSQL build-tag integration은 로컬 Docker 부재로 미실행이지만, 새 HEAD의 원격 Go CI에서 PostgreSQL16 integration이 PASS했습니다. feature의 DB/SQL diff는 최신 main 대비 없습니다.
  • scoped secret scan은 unknown secret이나 전체 Git history 검사가 아닙니다. cloud 재현은 실제 VM/network를 만들고 지우므로 외부 config/key와 명시적 opt-in이 필요합니다.

확인

  • LBT-147 전체 변경과 모든 review/audit finding을 기록했습니다.
  • 요구사항→구현→자동 테스트→실환경 Evidence 추적과 독립 검토를 수행했습니다.
  • Secret/Credential/Token 원문을 추가하지 않았습니다.
  • 새 HEAD cf19fee 원격 CI: Go·PostgreSQL16, Web, Contracts, Backend Image
  • CODEOWNER 최종 승인

@imcherry5778 imcherry5778 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request Changes — LBT-147 (reviewed exact HEAD ed7843e25be9)

최신 branch diff, 관련 production caller/consumer, LBT-147 Acceptance, D-18 v5 / D-21 v10, 테스트 및 게시 Evidence를 다시 대조했습니다. Go/Web/Contracts CI는 현재 HEAD에서 모두 성공했고, 로컬 OpenStack 검증 로그도 남아 있습니다. 다만 아래 **3건(핵심 P1 1건, P2 2건)**은 아직 소스 경로에서 해소되지 않아 승인할 수 없습니다. 아래 내용은 코드 경로에 근거한 리뷰 finding이며, 제가 이번 리뷰에서 해당 실패를 직접 재현했다고 주장하지 않습니다.

1. [P1] Channel 단위 취소가 같은 VM의 정상 SSH 세션까지 종료

vmaccess.Manager.OpenSession의 channel-open context callback(164행), vmaccess.Session.Setup의 callback(280행)이 모두 retire(connection)을 호출합니다. retireLocked는 공유 ssh.Client를 닫으므로, 이미 정상 실행 중인 PTY A가 있어도 별도 PTY/SFTP B의 NewSession/RequestPty/RequestSubsystem 지연 및 timeout/cancel만으로 A까지 끊길 수 있습니다. 현재 정상 Close 독립성 테스트와 단독 timeout 테스트는 이 동시성 조합을 검증하지 않습니다. 이는 LBT-147 완료 조건(채널 독립성·실패/취소 정리), D-18의 독립 PTY/SFTP 경계에서 중요한 공백입니다.

요청: 동일 VM의 기존 PTY A가 실제 I/O 중인 상태에서 신규 채널 B의 open/setup timeout 또는 cancel을 강제하고 A의 I/O와 연결이 유지되는지 검증하는 회귀 테스트를 추가해 주세요. 필요하다면 channel-only cancellation/cleanup 경계를 보완해 주세요. SSH 라이브러리 제약 때문에 transport 전체 폐기가 불가피하다면, 단순 PR 제약 기재만으로 완료 처리하지 말고 D-18/AC 대비 예외를 decision owner가 명시적으로 수용하고 영향을 추적해야 합니다.

2. [P2] Provider 조회 장애가 영구적인 INVALID_SESSION으로 잘못 분류

openstack.Adapter.ResolveVMAccess(33–35행)는 resolveServerAddress의 모든 non-context 오류를 vmaccess.ErrInvalidTarget으로 교체합니다. 예컨대 Nova GET 503/Neutron List 503도 Manager.OpenSession을 거쳐 wss.handleTerminalOpen에서 INVALID_SESSION이 됩니다. 실제 대상/generation 불일치와 일시적인 Provider 장애를 구분해야 재시도·사용자 오류 처리도 올바르게 동작합니다.

요청: 소유권/대상 불일치만 ErrInvalidTarget에 매핑하고, 조회·인증·도달성·timeout 등 운영 장애는 안전한 retryable/unavailable 계열로 구분해 주세요(외부로 raw Provider/SSH error를 노출하지 않아야 합니다). Nova/Neutron 503 및 진짜 mismatch의 Control 결과 코드 회귀를 추가해 주세요.

3. [P2] Shutdown defer 역순으로 종료 사유가 SSH_DISCONNECTED/PTY_EXITED로 경합

runControl(250–252행)의 defer TerminalManager.CloseAll("SERVICE_RESTARTING") 다음에 defer VMAccess.Close()를 등록하여 실제 실행은 VMAccess 종료 → TerminalSession 종료가 됩니다. 첫 번째 호출이 SSH transport를 끊으면 Session.startPtyPump의 종료 경로가 먼저 SSH_DISCONNECTED 또는 PTY_EXITED로 Session을 닫을 수 있고, closeOnce로 인해 나중의 SERVICE_RESTARTING이 덮어쓰이지 않습니다. 이는 확정된 D-21의 종료 원인 구분과 어긋납니다.

요청: shutdown 시 TerminalSession을 SERVICE_RESTARTING으로 명시 종료한 뒤 SSH Manager를 정리하도록 순서를 보장하고, active PTY+Data WSS를 둔 종료 경쟁 테스트에서 reason 및 ENDED 1회 발행을 검증해 주세요.


검증 요청: 수정 후 새 exact HEAD로 해당 동시성·오류 분류·shutdown 회귀, go test -race 및 관련 CI 결과를 공유해 주세요. 이번 PR의 로컬 OpenStack Evidence와 미검증 KT Cloud/LBT-145 선행조건은 별개로 유지하며, 이번 리뷰가 KT Cloud Acceptance 충족을 의미하지는 않습니다. 수정 사항을 반영하면 새 HEAD 기준으로 재리뷰하겠습니다.

Comment thread internal/connector/vmaccess/access.go Outdated

// x/crypto/ssh channel-open has no context. A stalled setup retires the
// transport to release all pending reads rather than leaving a goroutine.
stop := context.AfterFunc(ctx, func() { m.retire(c) })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 · 동일 근본 원인(1/2)] NewSession()이 stall된 상태에서 이 요청의 ctx만 만료돼도 m.retire(c)가 공유 ssh.Client를 종료합니다. 이미 동작 중인 다른 PTY/SFTP Channel이 같이 끊기는지 검증되지 않았습니다. 같은 VM에서 A=활성 I/O, B=stalling NewSession + cancel 회귀를 추가하고, 가능하면 B 실패만 격리해 주세요. 아래 Session.Setup에도 동일 문제가 있습니다.

Comment thread internal/connector/vmaccess/access.go Outdated
func (s *Session) Setup(ctx context.Context, setup func(*ssh.Session) error) error {
ctx, cancel := context.WithTimeout(ctx, s.manager.config.SetupTimeout)
defer cancel()
stop := context.AfterFunc(ctx, func() { s.manager.retire(s.connection) })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 · 동일 근본 원인(2/2)] B의 RequestPty/RequestSubsystem setup timeout/cancel이 s.manager.retire(s.connection)을 호출하여 이미 실행 중인 A의 공유 transport까지 닫습니다. 현재 테스트는 정상 Channel.Close 독립성만 보여줍니다. A가 정상 echo 중, B Setup이 stall/cancel된 뒤 A가 계속 echo 가능한지 회귀와 channel 격리(또는 승인된 명시적 예외)를 요청합니다.

}
address, err := a.resolveServerAddress(ctx, target.VMKey, target.ServerID, &target)
if err != nil {
return vmaccess.Endpoint{}, safeContextError(ctx, vmaccess.ErrInvalidTarget)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] resolveServerAddress()의 Nova GET 실패/Neutron List 오류(예: HTTP 503)까지 여기서 모두 ErrInvalidTarget이 됩니다. 그 결과 OpenSession→handleTerminalOpen이 일시적인 Provider 장애를 INVALID_SESSION으로 회신합니다. 진짜 lab/gen/VM mismatch만 invalid로 유지하고 운영 장애는 안전한 unavailable/retryable 분류로 전파해 주세요. Nova/Neutron 503 vs target mismatch Control 코드 테스트를 부탁드립니다.

Comment thread internal/connector/app/app.go Outdated
}
defer connectorApp.TerminalManager.CloseAll("SERVICE_RESTARTING")
if connectorApp.VMAccess != nil {
defer connectorApp.VMAccess.Close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] defer는 역순이라 현재 VMAccess.Close()가 TerminalManager.CloseAll("SERVICE_RESTARTING")보다 먼저 실행됩니다. 이때 PTY pump가 SSH 종료를 먼저 관측하면 SSH_DISCONNECTED/PTY_EXITED로 closeOnce가 확정될 수 있습니다. Terminal을 SERVICE_RESTARTING으로 먼저 닫고 SSH를 정리하도록 보장하고, active PTY/Data WSS 상태의 종료 사유·중복 ENDED 회귀를 추가해 주세요.

KT D1 Management Tier 조회와 LBT-147 target affinity 검증을 함께 유지한다.

Control 연결 소유권에 맞춘 종료 회귀와 backpressure 관측 안정화를 포함한다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants