Repository navigation
fix(source): 원격 prefer 연결의 암호화 판정을 서버 자기 보고 대신 클라이언트 TLS 협상 결과로 한다 - #49
Merged
Merged
Conversation
원인:
- 네트워크를 지나는 prefer(PG prefer, MySQL PREFERRED) 연결은 sqlx 자체 prefer로
연 뒤 after_connect에서 서버에 세션의 TLS 여부를 물었고(pg_stat_ssl, Ssl_cipher),
답이 암호화면 connection-not-encrypted limitation을 내지 않았다.
- 클라이언트 쪽 TLS만 거절하고 서버와는 TLS로 붙는 중간자 뒤에서는 서버의 답이
중간자-서버 구간을 말해 "암호화됨"이 되고, 클라이언트 평문 구간의 경보가 사라졌다.
가짜 서버는 답을 그냥 위조할 수도 있다. 평문으로 위조할 수 있는 답에 기댄 fail-open이다.
수정:
- 네트워크를 지나는 prefer 연결은 sslmode만 require(MySQL REQUIRED)로 바꿔 먼저 연다.
열리면 그 풀을 그대로 쓴다. 이후 연결도 sqlx가 TLS를 강제한다. 서버가 TLS를
거절했을 때만 disable(DISABLED)로 다시 열고 "the server did not accept TLS, so the
catalog was read over an unencrypted connection; <해결 방향>" limitation을 남긴다.
그 밖의 오류는 기존 접속 실패(종료 2)다.
- 로컬(Unix 소켓·루프백)·disable·require 이상은 그대로다. 서버 확인 질의
(observe_transport, transport_from_answer, transport_from_cipher), needs_server_check,
Transport::Unconfirmed와 "could not be confirmed" 문구를 지웠다.
- README 두 언어의 판정 설명과 Scripts/verify-native-tls.py의 기대값을 맞췄다.
결정: 평문 재시도는 sqlx가 서버의 TLS 거절에 내는 Error::Tls("server does not support
TLS")에서만 한다 (이유: sqlx 0.8.6에서 서버가 TLS를 받은 뒤의 핸드셰이크 실패는
Error::Io로 나와 결정의 Error::Tls 기준에 들지 않는다. 같은 Error::Tls의 나머지(서버
이름으로 쓸 수 없는 호스트, 클라이언트 인증서 오류)는 서버의 거절이 아니다. 여기서
평문으로 내려가면 사용자가 준 TLS 설정을 무시하고 limitation의 원인도 틀린다. 그래서
원칙 1(fail-closed)과 원칙 2(핸드셰이크 실패는 종료 2라는 기존 계약)를 따랐다. sqlx를
올려 문구가 바뀌면 거절도 종료 2가 되는 쪽으로만 틀린다.)
결정: 시도용 연결을 PgConnection/MySqlConnection::connect_with로 따로 열지 않고 풀의
첫 접속을 시도로 쓴다 (이유: 따로 열면 sqlx 풀이 하던 접속 거절·기동 중 오류의
재시도(acquire 상한 30초)가 원격 prefer에서만 사라져 기존 동작이 바뀐다(원칙 2).
성공하면 그 풀이 곧 require 풀이라 연결도 하나 덜 연다.)
검증:
- cargo test -p schemagraph-source -p schemagraph-cli 통과. 결정 함수(성공은 require
풀, 거절만 평문, 그 밖의 오류는 실패), open 흐름, 문구의 단위 테스트를 더했다. 가짜
서버 통합 테스트 3개(PG 거절 뒤 새 평문 접속, PG 핸드셰이크 실패 뒤 재접속 없음,
CLIENT_SSL 없는 MySQL 서버 뒤 새 평문 접속)를 더했다. 이 가운데 PG 거절과 MySQL
사례는 수정 전 코드에서 실패한다(거절된 접속으로 평문을 이어 간다).
- Scripts/verify-native-tls.py에 중간자 재현 사례(downgrade-proxy-network-host)를
더했다. 수정 전 엔진은 limitation 없이 scan에 성공해 이 사례에서 실패한다. 수정 뒤
PG16 17사례와 MySQL 8.4·MariaDB 11.4 14사례가 모두 통과한다.
- 수정 전후 엔진으로 PG fixture(basic.sql)를 넣은 평문·TLS 클러스터의 12사례와 SQLite
basic을 scan했다. 종료 코드는 모두 같다. graph.json은 TLS를 끈 서버의 원격 기본
prefer 하나만 다르고, 그 차이는 전송 limitation 문구뿐이다. verify-bridge-facts 결과도
같다. clippy 경고는 기준선과 같다(고유 29건).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인: - 3b8bef9가 네트워크를 지나는 prefer(MySQL PREFERRED) 연결의 판정을 서버의 자기 보고(pg_stat_ssl, Ssl_cipher) 대신 reader 자신의 TLS 협상 결과로 바꿨다. 그런데 Unreleased의 Security·Changed 문단은 서버에 묻는 방식과 "encryption could not be confirmed" limitation을 그대로 설명하고 있었다. 수정: - Security 다섯째 불릿: 기본 모드에서 원격 호스트는 sslmode=require(ssl-mode=REQUIRED) 처럼 먼저 붙고 서버가 TLS를 거절했을 때만 TLS 없이 다시 붙으며, 그때 limitation은 서버가 TLS를 받지 않았다고 말한다고 적었다. 판정이 서버 보고가 아니라 reader의 협상이라 reader 쪽 TLS를 거절하는 프록시가 limitation을 숨기지 못한다는 것과, limitation이 풀러·프록시의 다음 구간이 아니라 reader 자신의 연결을 말한다는 것도 README 두 언어와 같게 적었다. - Changed 첫 불릿에서 "or encryption could not be confirmed"를 지웠다. - 서버 확인 방식은 #47 뒤로 태그가 없어(원격 최신 태그 v0.7.1, git tag --contains 27f665e 비어 있음) 릴리스된 적이 없다. 그래서 새 불릿을 더하지 않고 기존 두 문단만 최종 동작으로 고쳤다. 버전 번호는 그대로다. 검증: - transport.rs의 limitation 문구, README.md·README.ko.md의 같은 문단, Scripts/verify-native-tls.py 사례(downgrade-proxy-network-host 등)와 대조했다. - 바뀐 줄은 모두 80자 이내이고 git diff --check가 깨끗하다. Co-Authored-By: Claude Opus 5.5 <[email protected]>
원인:
- Scripts/verify-native-tls.py의 DowngradeProxy.__exit__는 리스너를 닫아 serve 스레드의
accept를 끝내려 했다. Linux는 다른 스레드가 리스닝 소켓을 닫아도 막힌 accept를 깨우지
않는다(macOS는 ECONNABORTED로 깨운다). 그래서 CI verify job(ubuntu-24.04)에서는
join(timeout=10)이 상한을 다 기다려 TLS 검증 단계 실행마다 10초가 늘었다. 그 accept가
소켓을 붙잡아 프로세스가 끝날 때까지 프록시 포트도 LISTEN으로 남았다.
- serve 문서 주석("리스너를 닫으면 accept가 OSError로 끝난다")은 Linux에서 틀렸다.
사례 판정 결과는 바뀌지 않았다. 스레드가 daemon이고 판정이 종료 전에 끝나기 때문이다.
수정:
- 리스너에 ACCEPT_POLL_SECONDS(0.2초) timeout을 건다. __exit__는 threading.Event로
멈춤을 요청하고, serve가 accept에서 나와 끝나기를 기다린 뒤 리스너를 닫는다. 그래서
포트도 바로 놓인다.
- timeout은 socket.timeout으로 받는다. Python 3.9(macOS 기본 python3)에서는 이 예외가
TimeoutError의 별칭이 아니다. TimeoutError로 받으면 timeout이 일반 OSError로 떨어진다.
- 리스너는 serve가 끝난 뒤에 닫으므로, accept가 내는 다른 OSError는 실제 실패다. 예전처럼
조용히 끝내지 않고 errors에 남겨 사례 판정이 실패로 낸다. PROXY_STOP_SECONDS(10초) 안에
스레드가 멈추지 않을 때도 같은 방식으로 실패를 낸다. 이번 결함처럼 join 상한이 조용히
지나가지 않게 하기 위해서다.
- serve·__exit__ 문서 주석을 실제 동작에 맞췄다.
결정: 리스너를 닫기 전에 shutdown(SHUT_RDWR)을 부르는 대안은 쓰지 않았다 (이유: 실측해
보니 Linux는 shutdown 뒤 accept가 EINVAL로 깼다. 하지만 macOS는 리스닝 소켓의 shutdown이
ENOTCONN을 내고 accept를 깨우지 못했다. 플랫폼마다 다른 errno를 가려 받아야 한다. timeout과
Event는 두 플랫폼에서 같은 경로로 끝나고, 비용은 종료마다 최대 0.2초다.)
검증:
- 실제 DowngradeProxy를 열고 닫는 측정을 python:3.12-slim 컨테이너(Linux 6.8)에서 했다.
수정 전에는 __exit__가 10.01초 걸렸고, serve 스레드가 살아 있었고, 종료 뒤에도 포트가
LISTEN(/proc/net/tcp 0A)이라 새 접속이 받아졌다. 수정 후에는 0.12초에 스레드가 끝나고
접속이 거절된다. macOS(Python 3.14.7·3.9.6)는 수정 전 0.0초, 수정 후 0.09초 안팎이다.
- 가짜 TLS upstream으로 중계 경로(SSLRequest 거절 → 평문 StartupMessage → 서버와 TLS
중계)를 Linux·macOS에서 끝까지 확인했다. 수정 후 Linux 종료는 0.17초이고, 중계 결과는
수정 전과 같다.
- 변이 확인: 닫은 뒤 join하는 예전 순서는 Linux에서 "accept failed: OSError: [Errno 9]
Bad file descriptor"로 드러난다. 멈춤 요청을 보지 않는 serve는 두 플랫폼에서 "did not
stop within 10 seconds"로 드러난다.
- PostgreSQL 16으로 python3 Scripts/verify-native-tls.py --engine <debug 엔진>
--mysql-environments Fixtures/accuracy/mysql-environments.json 실행: rc 0, PG 17·MySQL
7·MariaDB 7사례, 21초. PG만 돌린 출력 JSON은 수정 전과 바이트 단위로 같다.
- pyflakes 3.2.0 지적 0건(수정 전후 같음). Python 3.14.7·3.9.6에서 -W error로 import가
통과한다.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Owner
Author
GLM 외부 리뷰 결과 (packet-ask, effort high)저장소 규칙(AGENTS.md)대로 PR 전체 diff를 GLM에 packet-ask로 보내 리뷰받았습니다. 결과는 지적 없음입니다. 그래서 보완 커밋도 push도 없고, PR head는 수행 범위기준점은
지적별 처리 (0건)GLM은 "지적 없음"으로 답하면서, 결함으로 보지 않은 근거도 함께 적었습니다. 그 가운데 판정에 직접 걸리는 네 가지는 코드, sqlx 원본, 실행으로 다시 확인했습니다.
다시 돌린 검사PR head
CI(
🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약
앞선 보안 리뷰에서 설계 결정이 필요해 보류했던 schemagraph 항목을 승인된 설계 결정에 따라 구현했습니다. 이 항목은 PR #47(네이티브 reader TLS)을 검토한 보안 리뷰의 후속입니다. 이 PR 범위는 1건이고 구현 1건, 부분 구현 0건, 미구현 0건입니다.
followup-tls-prefer-client-sidepg_stat_ssl·Ssl_cipher)에 기대어, 평문 연결에서connection-not-encrypted경고가 가려질 수 있음결함
after_connect에서 서버에 TLS 여부를 물었습니다. PG는pg_stat_ssl, MySQL·MariaDB는SHOW SESSION STATUS LIKE 'Ssl_cipher'입니다. 답이 "암호화"면 limitation을 내지 않았습니다(transport.rs의TlsPolicy::Prefer => observed?).downgrade-proxy-network-host).바뀌는 것
sslmode=prefer·ssl-mode=PREFERRED연결은 먼저 TLS를 요구해(require·REQUIRED) 엽니다. 서버가 TLS를 거절했을 때만 TLS 없이 새 연결로 다시 엽니다.RELEASE-NOTES.md# Unreleased의 기존 Security·Changed 불릿을 고쳐 적었습니다. 버전 번호는 그대로입니다.설계 결정
결정 문서(원칙 1, 클라이언트가 아는 사실로 판정)
ssl_mode만 require(PgSslMode::Require,MySqlSslMode::Required)로 바꿔 연결 하나를 먼저 열어 본다(PgConnection/MySqlConnection::connect_with).sqlx::Error::Tls(서버의 TLS 거절이나 핸드셰이크 실패)면 disable(PgSslMode::Disable,MySqlSslMode::Disabled)로 평문 풀을 연다. 그리고 원인을 담은 limitationconnection-not-encrypted: the server did not accept TLS, so the catalog was read over an unencrypted connection; <remedy>를 남긴다.observe_transport,transport_from_answer,Ssl_cipher조회),needs_server_check,Transport::Unconfirmed는 지운다. 문서의 판정 설명도 맞춘다.결정 문서의 문구와 다르게 정한 것이 두 가지 있습니다. 결정 문서가 내다보지 못한 sqlx 0.8.6의 동작 때문이고, 공통 원칙으로 정했습니다. 두 결정 모두 3b8bef9 커밋 본문의
결정:줄에도 적었습니다. 둘 다 출력 계약을 바꾸지 않습니다. 첫째는 결정 문서의 문구보다 fail-closed 쪽이고, 둘째는 기존 접속 재시도 동작을 지킵니다.Error::Tls("server does not support TLS")일 때만 한다 (3b8bef9)N을 받았을 때, MySQL은 서버 핸드셰이크에 CLIENT_SSL이 없을 때입니다(sqlx-postgres·sqlx-mysqlconnection/tls.rs).Error::Tls로 보았습니다. 하지만 sqlx 0.8.6에서 서버가 TLS를 받은 뒤의 핸드셰이크 실패(인증서·프로토콜·EOF)는complete_io를 거쳐Error::Io로 나옵니다. sqlx-coretls_rustls.rs와 실서버 stderr로 확인했습니다.Error::Tls의 나머지 경우는 서버의 거절이 아니라 클라이언트 쪽 설정 오류입니다. 서버 이름으로 쓸 수 없는 호스트(127.1등)와 클라이언트 인증서 오류가 여기에 듭니다. 여기서 평문으로 내려가면 사용자가 준 TLS 설정을 무시하고, limitation의 원인도 틀리게 적힙니다.PgConnection/MySqlConnection::connect_with로 연결 하나를 따로 열어 보라고 했습니다.pool/inner.rs). 기존 동작이 바뀌므로 원칙 2에 어긋납니다.transport.rs(TransportCheck::open,after_tls_attempt) 한 곳에 둡니다. 두 reader는ConnectMode를 자기 sslmode로 옮기기만 하고,options_for는 sslmode 말고는 URL 그대로입니다.함께 정한 것
git tag --contains 27f665e는 비어 있습니다. 그래서 서버에 묻던 방식은 릴리스된 적이 없습니다. 새 불릿을 더하지 않고## Security다섯째 불릿과## Changed첫 불릿만 최종 동작으로 고쳤습니다.## Breaking의 "기본 prefer에서 핸드셰이크가 실패하면 종료 2"는 새 동작에서도 맞아 그대로 두었습니다.결정:줄): 리스너를 닫기 전에shutdown(SHUT_RDWR)을 부르는 대안은 쓰지 않았습니다. 실측하니 Linux는 shutdown 뒤 accept가 EINVAL로 깨어났지만, macOS는 리스닝 소켓의 shutdown이 ENOTCONN을 내고 accept를 깨우지 못했습니다. 플랫폼마다 다른 errno를 가려 받아야 합니다. 대신 accept timeout(0.2초)과threading.Event를 써서 두 플랫폼이 같은 경로로 끝나게 했습니다. 비용은 종료마다 최대 0.2초입니다.계약 변경
결정 문서에 따라 의도한 사용자 계약 변경입니다. 같은 내용을
RELEASE-NOTES.md# Unreleased와 README 두 언어에 적었습니다. 아래는 main(#47)과 비교한 변경입니다. #47의 동작은 아직 릴리스되지 않았으므로, v0.7.1 사용자에게는 Unreleased에 적힌 TLS 항목(이번에 고친 설명)이 바뀌는 것의 전부입니다.sslmode=prefer와 MySQL·MariaDBssl-mode=PREFERREDscan은 먼저require·REQUIRED로 접속합니다. 서버가 TLS를 거절했을 때만 TLS 없이 새 연결로 다시 접속합니다. 거절된 연결로는 더 보내지 않습니다. 거절되면 TCP 연결이 하나 더 듭니다.connection-not-encrypted: the server did not accept TLS, so the catalog was read over an unencrypted connection; <remedy>connection-not-encrypted: the catalog was read over an unencrypted connection; <remedy>였습니다. 서버가 답하지 못하면connection-not-encrypted: the catalog connection could not be confirmed as encrypted (<원인>); <remedy>였습니다. 뒤의 문구는 더 나오지 않습니다.connection-not-encrypted:, 위치(document·graph limitations 맨 앞),<remedy>문구,catalog_complete는 그대로입니다.pg_stat_ssl과SHOW SESSION STATUS LIKE 'Ssl_cipher'질의를 더 보내지 않습니다.disable·allow·DISABLED,require이상 모드의 출력과 종료 코드는 그대로입니다.engine/Cargo.lock도 그대로입니다.변경
수정 (fix)
테스트 (test)
문서 (docs)
바뀐 파일은 8개(+863/−301)입니다. 대부분 테스트와 공격 재현 하네스이고, 운영 코드는
transport.rs판정부와 두 reader의 접속부만 바뀌었습니다.engine/source/src/transport.rs(TransportCheck::open,negotiate,after_tls_attempt, limitation 문구),postgres.rs·mysql.rs(connect,options_for, 서버 확인 질의 삭제)transport.rs단위 테스트,postgres.rs·mysql.rs테스트,engine/cli/tests/database_url_errors.rs(가짜 서버 통합 테스트 3개)Scripts/verify-native-tls.py(중간자 재현 사례downgrade-proxy-network-host,DowngradeProxy종료 수정)README.md,README.ko.md,RELEASE-NOTES.md머지 참고
.github/workflows/,engine/source/src/sql/*.sql,probe/,probe-go/,Fixtures/,Dockerfile,action.yml은 바꾸지 않았습니다.검증
로컬 macOS(arm64)에서 HEAD f21b194로 실행했고, MySQL·MariaDB와 일부 재현은 Linux 컨테이너(Colima)에서 돌렸습니다. origin/main(04b4a22)이 이미 이 브랜치의 조상이라 merge가 필요 없었습니다(PR 생성 직전
git fetch origin으로 확인). GitHub CI는 push 전이라 아직 돌지 않았습니다.HEAD f21b194 검사
git diff --check origin/main...HEAD: pass — 공백 오류 없음cargo fmt --manifest-path engine/Cargo.toml --all -- --check: pass — 출력 없음cargo build·cargo test --manifest-path engine/Cargo.toml --workspace --locked -j 3 -- --test-threads=3(XDG_CONFIG_HOME=<scratch>/xdg,SCHEMAGRAPH_CACHE_KEY·PGSSL*제거): pass — 23개 묶음에서 595개 통과, 실패 0, 39초. 격리용 XDG 디렉터리는 끝까지 비어 있었고~/.config/schemagraph도 생기지 않았습니다.cargo clippy --workspace --all-targets --locked --message-format=json(HEAD와 origin/main 임시 worktree 비교): pass — 경고 집합이 같습니다. 양쪽 모두 고유 28건·총 58건이고 추가 0·제거 0입니다. 비교 키는 (파일, lint, 메시지, 소스 줄)입니다.cargo clippy … -- -D warnings: 기존 실패 — HEAD와 origin/main 모두 rc 101이고 같은 지점(schemagraph-sourceunnecessary_unwrap, schemagraph-analysistoo_many_arguments등)에서 멈춥니다. CI에는 이 단계가 없습니다.test-mysql-ready,test-fixture-ports,test-postgres-fixture-diagnostics,test-postgres-fixture-auth,test-sqlserver-oracle-accuracy,test-benchmark-scale,test-probe-sqlite-benchmark,test-review-action,test-container,test-dml-accuracy,test-dml-producers): pass — 합계 115건 OKbash Scripts/test-publish-release-assets.sh: pass — 오프라인 릴리스 게시 36사례(macOS에서만 실행)Scripts/verify-native-tls.py, Python 3.9.6·3.14.7-I -B -W errorimport: pass — 지적 0건(origin/main도 0건), 두 버전 모두 경고 없이 import됩니다.cmp5개,verify-contracts.py(jsonschema 4.23.0·sqlglot 30.18.0),verify-competitive.py(14초),verify-cancellation.py,verify-html.py --screenshot(Explorer 화면 정상 렌더링), SARIF 스키마 sha256(c3b4bb2d…) 확인 뒤verify-review-action.py(findings=1, baseline=0, incomplete=2), SKILL 두 사본·schemagraph skillcmp, crate LICENSEcmpverify-review-action.py --prepare뒤GITHUB_WORKSPACE·RUNNER_TEMP·GITHUB_OUTPUT과action.yml입력으로Scripts/run-review-action.py실행): pass — CI 기대대로 rc 1(outcome failure),exit_code=1, json·sarif·markdown 출력이 비어 있지 않음verify-accuracy.py --sqlglot --suite all --strict(PostgreSQL 16): pass — chinook 23사례, pagila 20사례, 실패 0verify-external-imports.py --dbt(dbt-core 1.12.5, dbt-postgres 1.11.0): pass — status okverify-mysql-accuracy.py --sqlglot --strict: pass — MySQL 8.4·MariaDB 11.4 각 16/16, 13초verify-openlineage.py,verify-bridge-facts.py: pass — openlineage status ok(직접 7·간접 3·이벤트 4). bridge-facts rc 0이고, status partial은--isthmus를 주지 않아서입니다(CI와 같고 기존과 같음).verify-native-tls.py --mysql-environments Fixtures/accuracy/mysql-environments.json(CI와 같은 옵션): pass — PG 17사례(평문 6·TLS 11,downgrade-proxy-network-host포함), MySQL 7사례, MariaDB 7사례, 24초. 남은 컨테이너 0cargo package --workspace --locked -j 3: pass — crate 6개 Packaging·Verifying, 69초. yoke-derive 0.8.3 yanked 경고는 origin/main부터 있던 것입니다.gradle -p probe check shadowJar --max-workers=2(Java 17): pass — 테스트 55개, 실패 0test-accuracy-sql.py --jdbc-jar …,verify-probe-maven.sh: pass — Maven CLI가 없어 Gradle consumer 경로로 확인gofmt -l,go vet,go test,CGO_ENABLED=0 go build: passverify-catalog-dependencies,verify-sqlite-nullability,verify-collection-scope(native·go·jdbc),verify-schema-lint,verify-unused: pass — 9단계 모두 rc 0Scripts/verify-fixtures.sh(CI처럼^주의:줄을 실패로 판정): pass —verify-fixtures: OK, 주의 0, 86초(MySQL·MariaDB·MSSQL azure-sql-edge·Oracle 포함)verify-dml-producers.py --database sqlite|postgres|oracle(v0.4.3 aarch64-apple-darwin 배포본)과verify-dml-replay.py: pass — SHA256SUMS와 jar 해시를 CI 고정 해시와 대조, replay status ok·failures 0verify-sqlserver-oracle-accuracy.py --database oracle --strict: pass — 18사례, 정확도 실패 0bash -eo pipefail로 실행): pass — version=0.7.1·publish=false, release 빌드 88초, 추출 바이너리 smoke(cancellation·SQLite scan·Go probe·query), 압축 파일 구성, JDBC jar smokedocker build뒤docker run --rm … --version: pass — 128초, 출력schemagraph 0.7.1수정 전 실패와 회귀 검출
engine/cli/tests/database_url_errors.rs)remote_default_postgres_url_reconnects_without_tls_after_the_server_refuses_it: 수정 전 코드에서 실패합니다. 거절된 접속으로 평문을 이어 가서 접속 수가 1입니다(기대 2).remote_default_mysql_url_reconnects_without_tls_when_the_server_lacks_it: CLIENT_SSL이 없는 서버입니다. 같은 이유로 수정 전 코드에서 실패합니다.remote_default_postgres_url_does_not_retry_without_tls_after_a_failed_handshake: 계약 고정용이라 수정 전후 모두 통과합니다. 핸드셰이크 실패 뒤 평문 재접속이 없어야 합니다.transport.rs단위 테스트Error::Tls·Io·PoolTimedOut은 접속 실패입니다.TransportCheck::open흐름(모드별로 기록한 접속 순서)과 limitation 문구도 고정했습니다.postgres.rs·mysql.rs: URL별negotiates_tls표,options_for가 sslmode만 바꾸는지, 해결 방향 문구 전체를 고정했습니다.downgrade-proxy-network-host(Scripts/verify-native-tls.py의DowngradeProxy): reader 쪽 SSLRequest에는N을 답하고, 서버 쪽에는 검증된 TLS로 붙어 중계합니다. 수정 전 엔진은 서버의pg_stat_ssl이 ssl=true를 보고하므로 limitation 없이 scan에 성공해 이 사례에서 실패합니다. 수정 후에는 PG16 17사례와 MySQL 8.4·MariaDB 11.4 14사례가 모두 통과합니다.git archive로 따로 빌드해 위 red를 다시 확인했습니다.측정
<scratch>/compare_scans.py). 입력은Fixtures/postgres/basic.sql을 넣은 PostgreSQL 16 클러스터 12사례와 SQLite basic입니다.localhost, 네트워크 호스트0.0.0.0)와 모드(기본,require,disable)를 조합했습니다.the catalog was read over an unencrypted connection에서the server did not accept TLS, so the catalog was read over an unencrypted connection으로 바뀐 것뿐이고, 접두사와 나머지 그래프는 같습니다.verify-bridge-facts.py결과도 수정 전과 같습니다.DowngradeProxy.__exit__시간(f21b194): python:3.12-slim(Linux 6.8)에서 수정 전 10.01초 → 수정 후 0.12초입니다. 수정 전에는 serve 스레드가 살아 있었고 종료 뒤에도 포트가 LISTEN이었습니다. macOS(Python 3.14.7·3.9.6)는 수정 전 0.0초, 수정 후 0.09초 안팎입니다. 사례 판정은 바뀌지 않았고, PG만 돌린 출력 JSON은 수정 전과 바이트 단위로 같습니다.실행하지 않은 검사
verify-container.py --image …: not-run — 스크립트가 저장소 루트 아래 임시 디렉터리를 bind mount하는데, Colima VM은 홈만 마운트합니다. 이미지 빌드와--version실행은 위에서 통과했습니다.verify-sqlserver-oracle-accuracy.py가 Linux x86_64 Docker만 받고, 로컬 VM은 aarch64입니다.ibm-fixtures.yml(Db2·Informix): not-run — linux/amd64 privileged 컨테이너와 메모리 5g가 필요합니다. 로컬 VM은 aarch64, CPU 2개, 4GiB입니다. 이 PR은 IBM 경로를 바꾸지 않습니다.원격 CI에서 꼭 볼 단계
downgrade-proxy-network-host사례가 Linux 러너에서 처음 돕니다. main(04b4a22) CI에서 이 단계는 30초였습니다. f21b194가 없으면 중간자 종료가 Linux에서 10초를 더 기다리므로, 새 사례가 더해져도 그만큼 늘지 않았는지 봅니다.구현하지 않은 항목
후속 참고(이 PR 밖)
server does not support TLS와 일치할 때만 합니다. 문구가 바뀌면 거절도 종료 2가 되는 쪽으로만 틀리고, 위 가짜 서버 테스트 2개가 실패해 알려 줍니다. 그때SERVER_REFUSED_TLS를 새 문구에 맞춥니다.Scripts/verify-fixtures.sh는 컨테이너를docker rm -f로(볼륨 없이) 지워, 실행마다 익명 볼륨 6개(약 0.5GB)를 남깁니다. 프로브 쪽 스크립트라 이 PR에서는 고치지 않았습니다.리뷰
Scripts/verify-native-tls.py의DowngradeProxy.__exit__가 리스너를 닫으면 accept가 깨어난다고 가정함. Linux(CI)에서는 깨어나지 않아 실행마다join(timeout=10)을 10초 내내 기다리고, 프로세스가 끝날 때까지 프록시 포트를 붙잡음 → 고침(f21b194). 위 "측정"과 "설계 결정"을 참고하세요.except OSError: return은 오류를 조용히 삼켰습니다. 이제 accept가 timeout 말고 다른 OSError를 내거나 스레드가 상한 안에 멈추지 않으면errors에 남기고, 사례 판정이 실패로 냅니다.socket.timeout으로 받습니다. Python 3.9에서는 이 예외가TimeoutError의 별칭이 아니어서,TimeoutError로 받으면 회귀가 생깁니다.accept failed: OSError: [Errno 9]로 드러납니다. 멈춤 요청을 무시하는 serve는 두 플랫폼에서did not stop within 10 seconds로 드러납니다.complete_io를 거쳐Error::Io, 거절 문구는 PG·MySQLtls.rs에 그대로, REQUIRED와 PREFERRED의 인증서 검증 설정이 같음).main.rs의source::read하나를 거치고, MCP에는 DB 접속 경로가 없습니다.0.0.0.0)에 붙여 봤습니다.sslmode=require와verify-full은 접속 하나에 종료 2이고 평문으로 내려가지 않습니다. 기본 모드와 명시 prefer는 거절된 접속에 더 보내지 않고, 새 StartupMessage 접속으로 다시 붙습니다.Co-Authored-By)을 지켰고, 비공개 이름과 홈 경로가 diff와 메시지에 없습니다.🤖 Generated with Claude Code