브라우저에 google.com을 치면 무슨 일이 일어날까? (아니, 진짜로) - 네트워크편

Basix·2021년 4월 30일
42
post-thumbnail

기술 면접을 가게 되면 종종 듣게 되는 면접 질문입니다. 일반적으론 DNS, HTTPS에 대한 기본적인 작동 방식을 알고 있고 그걸 설명할 수 있는지 묻는 간단한 질문이고, 실제로도 그렇게 대답하면 아마 중간 이상은 갑니다. 하지만 그 뒤에서, 정말 브라우저가 하는 일은 무엇일까요? DNS로 IP 받아오고, HTTP 요청을 쓰는 것만이 웹 브라우저의 여정의 전부는 아니겠지요. 이 글에서는 제가 팔 수 있을 만큼 깊게 브라우저가 어떤 여정을 즐기고 오는지, 그리고 구글 웹 서버는 (아마도) 어떻게 답변해줄 지를 간단히 다뤄보겠습니다.

여기서 들어가는 깊이는 OSI 계층 기준으로 세션 계층 및 그 위 계층입니다. 그 아래 계층까지 다루는 건 제 지식의 깊이가 부족하고 글이 너무 길어지니, 이 쪽에 흥미가 있다면 다른 글을 찾아봅시다.

이거 사이트 주소 맞나?

먼저 브라우저는 사용자가 그냥 검색을 하고 싶었을 뿐인지, 아니면 사이트 주소를 입력한 것인지 확인해야 합니다. 자세한 부분은 브라우저마다 차이가 있을 수 있지만, 이론적으로는 RFC 1738에 따라 유효한 URL이 맞는지 확인합니다.

DNS 요청

URL의 host 부가 IP 주소였다면 필요 없는 과정이지만, 십중팔구 도메인 주소 (여기서는 google.com이죠) 를 입력하여 사이트를 접속하기 때문에, 이 도메인 주소가 가리키는 목적지를 확인할 필요가 있습니다.

이전에는 RFC 1035 등의 오래된 DNS 규격을 모두 사용하고 있었으나, 최근에 들어서는 조금이나마 DNS over HTTPS (RFC 8484)나 DNS over TLS (RFC 8310)과 같은 암호화된 DNS 쿼리 프로토콜을 이용하기도 합니다. 대표적으로 Firefox가 미국 등 일부 지역에 한해 Cloudflare의 1.1.1.1 서버를 DNS over HTTPS를 통해 사용합니다.

먼저 통상적인 DNS를 살펴보자면, 먼저 브라우저는 각 운영체제에 DNS 요청을 보내달라고 요청합니다. 운영 체제는 네트워크 설정에 따라 지정된 DNS 재귀 확인자의 주소 (일반적으로 따로 지정하지 않을 경우, DHCP에서 함께 제공되는 DNS 서버를 쓰는 경우가 일반적이며, 이는 보통 ISP의 DNS 서버입니다.)에 DNS 요청을 보냅니다.

DNS 요청을 받은 재귀 확인자 (여러분이 아시는 8.8.8.8이나 1.1.1.1 등) 이 요청을 받으면, 재귀 확인자는 먼저 루트 DNS 서버에 요청을 전송합니다. 루트 DNS는 현재 13개의 주소가 있는데, 이 13개라는 주소는 당시 DNS가 고안되었을 때 루트 DNS의 최대 개수를 13개로 제한했었기 때문입니다. 현재는 Anycast 라우팅을 통해 이 주소들로 들어온 요청을 전세계에 있는 DNS 루트 서버 중 한 곳으로 전송합니다.

우리는 google.com을 찾고 있지만, 루트 DNS는 바로 가는 길을 알려주는 대신 .com 도메인 전체를 관할하는 다른 DNS 서버의 주소를 알려줍니다. 그러면 DNS 재귀 확인자는 이 서버에 다시 요청을 보내고, 그러면 .com의 TLD DNS 서버는 google.com의 권한 있는 DNS 서버를 알려줍니다.

마지막으로 google.com의 권한 있는 DNS 서버에 재귀 확인자가 요청을 보내면, 일반적으로 A 레코드에 적힌 IP 주소를 반환하여 보내줄 것입니다. 이제 드디어 IP를 받았네요! 하지만 대부분의 경우는 DNS 재귀 확인자가 google.com의 레코드를 캐싱해뒀을 가능성이 더 높습니다.

DNS 재귀 동작을 설명하는 다이어그램

DNS over HTTPS 등의 암호화된 DNS 프로토콜을 사용하는 경우, 운영 체제가 해당 프로토콜을 완전히 지원하지 않는 경우가 있습니다. Firefox는 DNS over HTTPS를 브라우저 내에서 구현하여 직접 Cloudflare 나 타 DNS over HTTPS 재귀 확인자에게 요청을 직접 보낼 수 있습니다.

또한 사용하고 있는 TLD의 DNS 서버와 웹 사이트의 DNS가 모두 DNSSEC를 지원하는 경우, 서명을 이용해 자신이 받은 DNS 결과가 위조나 MITM 공격을 통해 가로채진 것이 아니라 확실한 DNS 응답임을 증명할 수 있습니다. 분량 상 이 글에서는 언급만으로 남겨둡니다.

HTTP

이후에 브라우저는 HTTP/1.1 요청을 받은 IP의 80번 포트에 보냅니다. 그러면 구글은 일반적으로 HTTPS로 연결을 변경하기 위해 302 Found와 함께 HTTPS로 사용자를 리다이렉트합니다. (직접 실험해 본 결과 구글의 리다이렉트 여부는 User-Agent에 따라 달라지는 것 같습니다.) 그러면 브라우저는 응답에 있는 Location 헤더의 URL로 리다이렉팅을 시도하고, 이후 HTTPS로 접속을 재시도하게 됩니다.

TLS와 프로토콜 결정

TLS 핸드쉐이크 과정을 나타낸 다이어그램

여러분은 어떻게 브라우저가 사이트가 HTTP/2를 지원하는 지 알고 프로토콜을 변경할 수 있는지 아시나요? 이는 TLS의 ALPN 확장을 통해서 이루어집니다. ALPN은 RFC 7301에서 정의되는 TLS의 확장으로, 기존에 같은 목적으로 쓰였던 NPN을 대체하기 위해 작성되었습니다.

브라우저가 HTTPS 연결을 만들기 위해서는 먼저 TLS 연결이 만들어져야 합니다. TLS 연결은 TLS handshake 과정을 통해서 이루어지는데, 이 과정에서 사용할 암호화 알고리즘과 TLS 버전 등이 전송되며 서버와의 통신에서 어떤 버전과 암호화 알고리즘을 사용할 지 결정합니다.

이 때, ALPN을 지원하는 브라우저는 TLS 서버에 사용 가능한 알고리즘과 TLS 버전을 보내는 ClientHello 과정에서 지원하는 애플리케이션 프로토콜 목록을 추가합니다. 서버는 이 TLS ClientHello를 받고, h2가 프로토콜 목록에 있으며 서버가 HTTP/2를 지원하는 경우 HTTP/2로 통신하게 됩니다.

이를 알게 되면 왜 HTTP/2가 TLS가 활성화된 사이트에서만 지원되는지도 함께 알 수 있게 됩니다. HTTP/2로 전환하는 과정이 TLS를 동반하기 때문에, 일반적인 HTTP/1.1에서 HTTP/2로 바꿀 수 없는 것이죠. 물론 명세 상으로는 Upgrade 헤더를 통해 암호화되지 않은 HTTP/1.1을 HTTP/2로 업그레이드할 수 있도록 고안되어 있지만, 모질라 파이어폭스와 구글 크롬이 TLS에서만 HTTP/2롤 지원하도록 결정하면서 사실상 사용되지 않는다 보아도 무방합니다. 이는 보안상의 이유 뿐 만 아니라, 일부 중간 시스템이 80 포트에서 HTTP/1.1을 상정하고 HTTP/2 통신을 손상시킬 우려가 있었기 때문입니다.

요청 보내기

이제 TLS 핸드쉐이크가 성공적으로 끝났으니 이 시점에서 HTTP 요청을 보낼 수 있습니다. HTTP 버전은 앞에서 ALPN을 통해 정해진 HTTP 버전을 사용하게 됩니다. 다만 데이터 표현을 제외하고 HTTP/1.1과 HTTP/2에서 제공하는 헤더의 시멘틱은 거의 동일하므로, 이 글에서는 따로 구분 없이 설명하겠습니다.

GET / HTTP/1.1
Host: www.google.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ko-KR,ko;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
TE: Trailers

Upgrade-Insecure-Requests는 클라이언트에서 TLS 연결을 통한 암호화된 연결을 지원한다는 의미를 가집니다. 하지만 우리는 이미 TLS 연결을 수립했는데 왜 이 해더가 붙을까요? 그 이유는 이 해더가 CSP 규칙 중 하나인 upgrade-insecure-requests를 지원한다는 의미도 내포하기 때문입니다. 서버에서 이 CSP 규칙을 적용하게 되면 브라우저는 문서에 포함된 URL이 http로 표기되어 있더라도 HTTPS 요청을 수립합니다.

Connection 헤더는 일반적으로 close, keep-alive 중 하나로 작성되는데, close를 쓰면 응답을 받은 후 TCP 및 TLS 통신이 완전히 종료됩니다. 즉 새로 리소스를 받을 때마다 TCP와 TLS의 핸드셰이킹 과정을 거치는 것인데, 구현은 간편하나 다소 비효율적이죠. 따라서 HTTP/1.1에서는 한 연결을 재활용할 수 있도록 keep-alive가 등장했고, 기본적으로 keep-alive를 사용해 통신합니다. HTTP/2는 HTTP/1.1과 다르게 연결이 관리되므로, Connection 및 관련한 헤더들은 TE를 제외하고 모두 무시됩니다.

TE 헤더는 클라언트가 사용하는 Transfer Encoding을 나타냅니다. Trailers를 설정하게 되면 chunked 방식으로 HTTP/1.1을 사용할 때, 일부 헤더를 모든 데이터를 전송한 뒤 맨 끝에 표시할 수 있게 허용합니다. HTTP/2는 chunked가 아닌 자체적인 스트리밍을 사용하는데, 이 경우에도 Trailers, 즉 모든 데이터가 보내진 이후에 별개의 헤더를 보낼 수 있습니다.

컨텐츠 협상

Accept로 시작하는 헤더들은 Content negotiation에 사용되는 헤더입니다.

그런데 컨텐츠 협상은 뭘까요? 컨텐츠 협상은 HTTP에서 여러 가지 문서의 버전 중 사용자에게 가장 알맞는 버전을 제공하기 위해 사용자의 브라우저와 협상하는 매커니즘입니다.

현재 주로 사용되는 방식은 서버 주도 협상 방식으로, 위에 헤더처럼 사용자가 선호하는 컨텐츠 타입, 언어, 압축 포맷 등을 사용자가 요청을 보낼 때 헤더를 통해 함께 전달합니다. 그러면 서버는 이 요청 사항에 가장 알맞는 버전을 사용자에게 제공합니다. 이 '알맞는' 리소스를 선택하는 과정은 딱히 표준으로 지정된 것은 아니며, 서버가 가지고 있는 리소스와 상황에 맞춰 알맞게 구성합니다.

이러한 컨텐츠 협상에 사용되는 헤더는 Accept, Accept-Language, Accept-Encoding, Accept-Charset 등등이 있으며 최근 Client Hints라고 하는 구글에서 밀고 있는 User Agent의 대체제도 이 컨텐츠 협상 헤더의 일부로 활용됩니다.

또한 비권장되는 방식이지만 정말 흔하게 User-Agent가 사용할 리소스 선정에 영향을 주는 경우도 있으며, 이로 인해 User-Agent를 실질적으로 컨텐츠 협상 매커니즘에 포함된다 보기도 합니다.

응답 받기

HTTP/2 200 OK
date: Tue, 27 Apr 2021 02:03:56 GMT
expires: -1
cache-control: private, max-age=0
content-type: text/html; charset=UTF-8
strict-transport-security: max-age=31536000
content-encoding: br
server: gws
content-length: 33633
x-xss-protection: 0
x-frame-options: SAMEORIGIN
alt-svc: h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"

이 아래에 들어갈 데이터는 다음 글에서 살펴보고, 먼저 앞 내용을 살펴봅시다. 먼저 내용들이 대부분 소문자인데요, 그 이유는 HTTP/2는 모든 헤더명을 소문자로 작성하기 때문입니다.

천천히 살펴볼까요? 먼저 Date는 응답이 작성된 시간을 의미합니다. ...그게 끝입니다.

그 뒤에는 캐싱을 위한 몇 가지 옵션이 있는데, Expires는 해당 문서의 만료일을 표기합니다만 Cache-Control에 max-age 등이 있으면 무시되는 헤더입니다. 그러므로 Cache-Control을 살펴보자면 여러 옵션을 지정할 수 있는데, private는 한 사용자에게 특정된 자료임을 의미하고, max-age는 받은 응답이 최신본이라고 여겨지는 시간으로, 초 단위로 표기됩니다.

Strict-Transport-Security는 HSTS에 사용되는 헤더입니다. HSTS는 이 사이트에 사용될 보안 인증서를 고정하고, 반드시 HTTPS로 접속하도록 강제하는 헤더입니다. 이 헤더가 지정되면 공격자가 다른 인증서를 이용해 MITM 공격을 시도하거나, HTTP로 접속하여 MITM 공격에 노출되는 것을 막을 수 있습니다. max-age는 이 HSTS 규칙이 유효한 시간을 가리킵니다.

Content-Encoding은 응답을 압축할 때 사용된 알고리즘을 나타냅니다.

약칭설명
gzipGNU zip 에서 사용되는 알고리즘인 deflate을 이용해 압축합니다.
compressUNIX compress에 사용되는 LZW 알고리즘을 이용해 압축합니다.
deflatedeflate 알고리즘을 이용해 압축하나, gzip과는 형태에 차이가 있습니다.
brBrotli 알고리즘으로 압축합니다.
zstdZstandard로 압축합니다.

gzip과 Brotli가 흔히 쓰이는 편인데, Brotli가 구글의 작품이라 그런지 구글 웹사이트에서는 Brotli를 통해 웹사이트를 압축해 전송하고 있습니다.

Server 헤더는 웹사이트가 사용하고 있는 웹 서버의 이름을 가리킵니다. 여기서는 gws인데, 어쩌면 예측하셨을 지 모르지만 Google Web Server입니다...

Content-Length는 응답의 길이를 바이트 수로 나타냅니다. 이 헤더가 존재하는 이유는 HTTP/1.1부터 한 연결을 재사용하기 때문인데요, 응답이 끝난 건지 아니면 아직 보낼 데이터가 남았는데 안 온건지를 정확히 판단하기 어려우니 Content-Length를 통해 데이터의 길이를 확인하고, 모든 데이터가 도착했으면 연결을 재사용할 준비가 되었다고 판단할 수 있는 것이죠.

X-XSS-Protection은 Internet Explorer, Chrome, Safari에서 지원하는 확장 헤더로 브라우저에서 Reflected XSS 공격을 감지했을 때 자동으로 사이트의 로드를 중단해주는 브라우저 기능입니다. 최근에는 CSP 등으로 더욱 강력한 정책 설정이 가능해지며 사장되어 가는 추세이며, 구글도 이를 반영하였는지 기능을 끄도록 명시되어 있습니다.

X-Frame-Options는 이 응답이 프레임의 일부로 사용될 수 있는지를 지정하게 해주는 헤더입니다. 앞에 X가 붙어 있긴 하지만 RFC 7034로 표준의 일부이며, SAMEORIGIN을 사용하면 같은 Origin (대충 사이트와 동의어로 보시면 됩니다만, 자세한 설명은 MDN 관련 문서를 참조해주세요.)에서만 iframe, embed 태그 등으로 사용 가능하고, DENY를 사용하면 모든 프레임 사용이 불가능하도록 설정됩니다.

HTTP/3

어쩌면 기술 트렌드에 빠삭한 독자라면 이 글에서 HTTP/3에 관한 언급을 지금까지 하지 않았음을 눈치챘을 수도 있습니다. 이 이유는 HTTP/3의 핸드셰이킹 과정이 HTTP 응답을 받은 이후에 일어나기 때문입니다.

이 이유는 HTTP/3은 이전 HTTP 버전과는 다르게 TCP가 아닌 UDP를 기반으로 구현된 QUIC 프로토콜로 구현되어 있고, 그렇기 때문에 TCP 위 TLS의 핸드셰이킹 과정에서 ALPN으로 HTTP/3으로 연결할 수 없기 때문입니다.

그렇기 때문에 HTTP/3 엔드포인트는 HTTP/2의 확장인 Alt-Svc 헤더를 통해 이루어집니다. HTTP 서버는 이 헤더를 통해 HTTP/3 엔드포인트의 존재를 알릴 수 있고, 이를 확인한 브라우저는 (HTTP/3을 지원한다면) 이 헤더에 적힌 포트를 통해 HTTP/3 연결을 수립할 수 있습니다.


여기까지 google.com에 접속하면서 일어나는 일련의 네트워크 과정에 대해 알아보았습니다. 다음 글에서는 HTML 문서를 받고 이를 DOM 트리로 만드는 과정 등등에 대해서 다뤄 볼 생각입니다. 혹여 글을 읽으시던 중 이상한 부분이나 오류를 발견하셨다면 망설이지 마시고 댓글로 알려주시면 확인 후 참고하도록 할게요!

profile
꼬꼬마 개발자 워너비

2개의 댓글

comment-user-thumbnail
2021년 5월 2일

좋은 글 감사합니다😊

답글 달기
comment-user-thumbnail
2021년 5월 11일

감사합니다 :)

답글 달기