ããã«ã¡ã¯ãã¯ã©ã¦ãåºç¤æ¬é¨ã®é島ã§ãã
ä»å¹´ã®ã¤ã³ã¿ã¼ã³ã·ããã§ã¯ããã©ãããã©ã¼ã (èªç¤¾åºç¤)ã³ã¼ã¹ã¨ãã¦2åã®æ¹ãåãå ¥ããããããç°ãªã課é¡ããã£ã¦ãããã¾ããã ãã®ãã¡ã®ä¸ã¤ã¯ Pingora ã«é¢ãã課é¡ã§ãè¦éããã«åãçµãã§ããã ãã¾ãããï¼ããä¸ã¤ã®èª²é¡ã¯ nginx ã®ãã£ãã·ã¥ã®æ§è½ã«é¢ãããã®ã§ãããã«ã¤ãã¦ã¯æ¨æ¥ã®è¨äºããåç §ãã ããï¼
Pingora 㯠Cloudflare ãéçºãããã¼ããã©ã³ãµã®ããã®ãã¬ã¼ã ã¯ã¼ã¯ã§ãããRust ã使ã£ã¦å¥½ããªãã¸ãã¯ãçµã¿è¾¼ãã ãã¼ããã©ã³ãµãæ¸ããã¨ãã§ãã¾ãã ä»åã®ã¤ã³ã¿ã¼ã³ã§ã¯ Pingora ã使ã£ã¦ TLS ã®ã¯ã©ã¤ã¢ã³ã証ææ¸ã使ã£ãèªè¨¼ãããã·ãä½ã£ã¦ãããã¾ããã ããã§ããã®éçºã®ä¸ã§å¾ããã Pingora ã OpenSSL ã«é¢ããç¥è¦ãå ±æãããã¨æãã¾ãã
ãã®è¨äºã¯è¦éããã®ã¤ã³ã¿ã¼ã³ã«ãããææããµã¤ãã¦ãºã®ç¤¾å¡ã§ããé島ãã¾ã¨ãããã®ã§ãã ãã®è¨äºã¯ Rust ã®åºæ¬çãªç¥èãåæã¨ãã¦ãã¾ãã
èæ¯
ç§ãã¡ã¯ nginx ãã¡ã¤ã³ã®ãã¼ããã©ã³ãµã¨ãã¦å©ç¨ãã¦ãã¾ãã ãããã¯ã·ã§ã³ç°å¢ã«ããããã¼ããã©ã³ãµã«ã¯å¤ãã®æ©è½ãæ±ãããã¾ãã åçãã£ã¹ããããåæãªã¯ã¨ã¹ãæ°å¶éãèªè¨¼ã»èªå¯ãç¹æ®ãªã«ã¼ãã£ã³ã°ãTLSã»ãã·ã§ã³åæãªã©ãæã ã¯å¤ãã®æ©è½ã nginx ã®è¨å®ãã¡ã¤ã«ã®ä¸ã«å®è£ ãã¦ãã¾ããã é·å¹´åãç¶ããã¦ãã nginx ã®è¨å®ãã¡ã¤ã«ã¯ãã¯ãããã°ã©ã ã¨ããã¹ãè¤éãã§ãããè¨å®ãã¡ã¤ã«ã§ãããããã«ãã«ãã»ã«åãåãã§ãã¯ãåä½ãã¹ãã¨ãã£ãç¾ä»£ã®ããã°ã©ãã³ã°è¨èªã®æ©æµã«é ãããã¨ãã§ãã¾ããã ããã¾ã§ããã¨ããã£ãã®ãã¨ãã¼ããã©ã³ãµãå ¨é¨ããã°ã©ã ã¨ãã¦æ¸ãã¦ãã¾ãã°ããã®ã§ã¯ãªãããã¨æãã¦ãã¾ãã
ãã㧠Pingora ã§ãã
Pingora 㯠Cloudflare ã OSS ã¨ãã¦å ¬éãã¦ãããã¼ããã©ã³ãµã§ãã ããæ£ç¢ºã«ããã¨ãPingora ã¯ãã¼ããã©ã³ãµã®ããã® ãã¬ã¼ã ã¯ã¼ã¯ ã§ãããRust ã使ã£ã¦å¥½ããªãã¸ãã¯ãçµã¿è¾¼ãã ãã¼ããã©ã³ãµãæ¸ããã¨ãã§ãã¾ãã Rust ã§æ¸ãã¨ãããã¨ã¯ãåå®å ¨æ§ãã¡ã¢ãªå®å ¨æ§ã並è¡æ§ãããã©ã¼ãã³ã¹ã¨ãã£ã Rust ã®ç¹å¾´ããã¼ããã©ã³ãµã«ãæã¡è¾¼ããã¨ãã§ããã¨ãããã¨ã§ãã
æã ã¯æã ã®è¤é㪠nginx ãããã¡ã³ããã³ã¹ããããå½¢ã«ç§»è¡ã§ããªããèãã¦ãã¾ãã ããã§ãä»åã¯ãµã¤ãã¦ãºç¤¾å 㧠"Skylab" ã¨å¼ã°ãã¦ããæ©è½ã®å®è£ ã Pingora ã§è¡ã£ã¦ã¿ã¾ããã "Skylab" ã¯ãTLS ã®ã¯ã©ã¤ã¢ã³ã証ææ¸ãåçã«ãã¼ããã CA ãç¨ãã¦è¡ãæ©è½ã§ããµã¤ãã¦ãºã® nginx ã®ä¸ã§ãæãè¤éãªãã®ã®ä¸ã¤ã§ãã
Pingora å ¥é
Pingora ãã©ã®ãããªãã®ããç°¡åãªä¾ãéãã¦è¦ã¦ããã¾ãããã ãªãããã®è¨äºã®ä¾ã¯ Pingora 0.3 ã§åä½ç¢ºèªãã¦ãã¾ãã
Hello, World
Pingora ãã©ããªæããªã®ããã¾ãã¯è§¦ã£ã¦ã¿ã¾ãããã
以ä¸ã« Hello, world!
ãè¿ã Web ãµã¼ãã¼ã Pingora ã§æ¸ããä¾ã示ãã¾ãã
åºå®ã®ã¬ã¹ãã³ã¹ãè¿ãã ãã ã¨é¢ç½ããªãã®ã§ãã¢ã¯ã»ã¹ã«ã¦ã³ã¿ã¼ãä»ãã¦ãã¾ãã
//! ```cargo //! [dependencies] //! async-trait = "0.1" //! env_logger = "0.11" //! http = "1" //! log = "0.4" //! pingora = { version = "0.3", features = [ "lb" ] } //! ``` use std::sync::atomic::{AtomicU64, Ordering}; use async_trait::async_trait; use http::header::{CONTENT_LENGTH, CONTENT_TYPE}; use http::{Response, StatusCode}; use pingora::apps::http_app::ServeHttp; use pingora::protocols::http::ServerSession; use pingora::server::Server; use pingora::services::listening::Service; struct HelloApp { counter: AtomicU64, // ã¢ã¯ã»ã¹ã«ã¦ã³ã¿ã¼ } #[async_trait] impl ServeHttp for HelloApp { async fn response(&self, _server_session: &mut ServerSession) -> Response<Vec<u8>> { let n = self.counter.fetch_add(1, Ordering::SeqCst); let message = format!("Hello, world!\r\nããªã㯠{n} 人ç®ã®è¨ªåè ã§ãï¼\r\n").into_bytes(); Response::builder() .status(StatusCode::OK) .header(CONTENT_TYPE, "text/plain") .header(CONTENT_LENGTH, message.len()) .body(message) .unwrap() } } fn main() -> pingora::Result<()> { env_logger::init(); let mut server = Server::new(None)?; server.bootstrap(); let hello_app = HelloApp { counter: AtomicU64::new(1), }; let mut hello_service = Service::new("hello app".to_owned(), hello_app); hello_service.add_tcp("[::]:3000"); server.add_service(hello_service); server.run_forever(); }
ããã§ã¯å®è¡ãã¦ã¿ã¾ãããããããã°ãã°ãåºããã»ããé¢ç½ãã®ã§ RUST_LOG=debug
ãä»ãã¦å®è¡ãã¾ãã
$ RUST_LOG=debug cargo run
å¥ã®ç«¯æ«ãã http://localhost:3000/
ã«ã¢ã¯ã»ã¹ããã¨ãHello, world!
ã¨ã¢ã¯ã»ã¹ã«ã¦ã³ã¿ã¼ã表示ãããã¯ãã§ããã¢ã¯ã»ã¹ãããã³ã«ã«ã¦ã³ããå¢ãã¦ãããã¨ã確èªãã¦ãã ããã
$ curl -i localhost:3000 HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 59 Date: Thu, 19 Sep 2024 11:55:43 GMT Connection: keep-alive Hello, world! ããªã㯠1 人ç®ã®è¨ªåè ã§ãï¼
ãã®ããã«ãPingora ã§ã¯æ¬¡ã®ãããªæé 㧠HTTP ãªã¯ã¨ã¹ããå¦çãããµã¼ãã¼ãæ¸ããã¨ãã§ãã¾ãã
- ServeHttp ãã¬ã¤ããèªåã§å®ç¾©ããåã«å®è£ ãã
- Service ã§ãã®åãã©ãããã
- Server ã«
Service
ã追å ãã
æ¬çã¨ã¯å°ãå¤ãã¾ãããã¢ã¯ã»ã¹ã«ã¦ã³ã¿ã¼ã®å¤ãä¿æããå¤æ° counter
㯠AtomicU64
ã¨ãã¦å®£è¨ããã¦ãã¾ãã
ãããæ®éã® u64
ã¨ãã¦å®£è¨ããã¨ãã³ã³ãã¤ã«ã¨ã©ã¼ã«ãªãã¾ãã
AtomicU64
ã¯ã¹ã¬ããéã§å
±æãã¦åæã«èªã¿æ¸ããã¦ãå®å
¨ãªã®ã«å¯¾ãã¦ãu64
ã¯å
±æã¨æ¸ãè¾¼ã¿ã両ç«ã§ããªãããã§ãã
ãã®ããã« Rust ã¯ãã«ãã¹ã¬ããã®ãããããã°ãã³ã³ãã¤ã«æã«æ¤åºãããã¨ãã§ãããå®å
¨æ§ã®é«ãè¨èªã§ããã¨è¨ããã¨ãã§ããã¨æãã¾ãã
ãªãã¼ã¹ãããã·
Hello, World ã®ä¾ã¯å ¨ããã¼ããã©ã³ãµæãããã¾ããã§ããã ããã§æ¬¡ã¯ãªãã¼ã¹ãããã·ãå®è£ ãã¦ã¿ã¾ãããã 以ä¸ã®ä¾ã¯ 8000 çªãã¼ãã«ãªãã¼ã¹ãããã·ãç«ã¦ããªã¯ã¨ã¹ãã 3000 çªãã¼ãã«è»¢éãããã®ã§ãã
//! ```cargo //! [dependencies] //! async-trait = "0.1" //! env_logger = "0.11" //! http = "1" //! log = "0.4" //! pingora = { version = "0.3", features = [ "lb" ] } //! ``` use async_trait::async_trait; use pingora::prelude::*; struct LB; #[async_trait] impl ProxyHttp for LB { type CTX = (); fn new_ctx(&self) -> Self::CTX { () } async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> { let peer = HttpPeer::new("127.0.0.1:3000", false, "".to_string()); Ok(Box::new(peer)) } } fn main() -> pingora::Result<()> { env_logger::init(); let mut my_server = Server::new(None)?; my_server.bootstrap(); let mut lb = http_proxy_service(&my_server.configuration, LB); lb.add_tcp("[::]:8000"); my_server.add_service(lb); my_server.run_forever(); }
å
ç¨ã® Hello, World ã®ä¾ãèµ·åããã¾ã¾ããã®ãªãã¼ã¹ãããã·ãèµ·åãã¦ã¿ã¾ãããã
ããã¦ãhttp://localhost:8000/
ã«ã¢ã¯ã»ã¹ãã¦ã¿ã¦ãã ããã
Hello, World ã®ã¬ã¹ãã³ã¹ããªãã¼ã¹ãããã·ããè¿ã£ã¦ããã¯ãã§ãã
ååã®ä¾ã¨ã®éã㯠ServeHttp
ã§ã¯ãªã ProxyHttp ãå®è£
ãã¦ãããã¨ã§ãã
ProxyHttp
ã¯ãªãã¼ã¹ãããã·ã®ããã®ãã¬ã¤ãã§ãupstream_peer
ã¡ã½ããã§ãªã¯ã¨ã¹ãã転éããå
ãæå®ãããã¨ãã§ãã¾ãã
ãã®ä¾ã§ã¯ upstream_peer
ã§åºå®ã®ããã¯ã¨ã³ããè¿ãã¦ãã¾ããããã¡ããåçã«ããã¯ã¨ã³ããå¤ãããã¨ãå¯è½ã§ãã
ä¾ãã°ãå¥ã®ãµã¼ãã¼ã«ããã¯ã¨ã³ããåãåããããã®çµæã«å¿ãã¦ããã¯ã¨ã³ããåçã«å¤ããã¨ãã£ããã¸ãã¯ãæ¸ããã¨ãã§ãã¾ãã
Pingora ã«ã¯ upstream_peer
ãå®è£
ããããã§ä¾¿å©ãª struct ã trait ãç¨æããã¦ãã¾ãã
- ããã¯ã¨ã³ãã®é¸æãã¸ãã¯ãæä¾ãã LoadBalancer
ããã使ãã¨ãã«ã¹ãã§ãã¯ãã©ã¦ã³ãããã³ãªã©ãç°¡åã«çµã¿è¾¼ããããã«ãªã£ã¦ãã¾ãã - ããã¯ã¨ã³ããåå¾ããããã®ãã¬ã¤ãã§ãã ServiceDiscovery
ãããå®è£ ãããã¨ã§ã好ããªãã¸ãã¯ã§ããã¯ã¨ã³ãã®ãªã¹ããåå¾ãããã¨ãã§ãã¾ãã
LoadBalancer ã ServiceDiscovery ã使ã£ãä¾ã¯å°ãé·ããªã£ã¦ãã¾ã£ãã®ã§ GitHub ã¸ã®ãªã³ã¯ãè²¼ã£ã¦ããã¾ãã
https://github.com/nojima/hello-pingora/blob/main/examples/04-dynamic-improved.rs
"Skylab" ã«ã¤ãã¦
Pingora ã®é°å²æ°ãæ´ãã ã¨ããã§ããã®è¨äºã®ã´ã¼ã«ã§ãã Skylab ã«ã¤ãã¦èª¬æãã¾ãã ãã®ããã«ã¯ã¾ã mTLS ã«ã¤ãã¦èª¬æããå¿ è¦ãããã¾ãã
é常ãTLS ã使ã£ãéä¿¡ã§ã¯ã¯ã©ã¤ã¢ã³ãããµã¼ãã¼ã®è¨¼ææ¸ãæ¤è¨¼ãã¾ãã ããªãã¡ãã¯ã©ã¤ã¢ã³ãã¯ãµã¼ãã¼ãæ示ãã証ææ¸ã¨ç½²åãæ¤è¨¼ãããã¨ã§ããµã¼ãã¼ãæ¬å½ã«ãã®ãã¡ã¤ã³ã®ææè ã«ãã£ã¦éç¨ããã¦ãããã¨ã確ãããã®ã§ãã
ä¸æ¹ãTLS ã§ã¯éã«ãµã¼ãã¼ãã¯ã©ã¤ã¢ã³ãã«å¯¾ãã¦è¨¼ææ¸ãè¦æ±ãããã¨ãã§ãã¾ãã ããã«ãããã¯ã©ã¤ã¢ã³ããæ¬å½ã«æå³ããã¯ã©ã¤ã¢ã³ãã§ãããã¨ããµã¼ãã¼ã¯æ¤è¨¼ãããã¨ãã§ãã¾ãã ãã®ã¨ãã¯ã©ã¤ã¢ã³ãããµã¼ãã¼ã«å¯¾ãã¦æåºãã証ææ¸ã ã¯ã©ã¤ã¢ã³ã証ææ¸ ã¨å¼ã³ã¾ãã
ãã®ããã«ã¯ã©ã¤ã¢ã³ãã¨ãµã¼ãã¼ããäºãã«ãäºããèªè¨¼ããããã¨ã¯ mTLS (mutual TLS) ã¨å¼ã°ãã¾ãã Skylab 㯠mTLS ã cybozu.com ã®è¿½å ã®èªè¨¼ã¨ãã¦å©ç¨ããæ©è½ã§ãã
ã¯ã©ã¤ã¢ã³ã証ææ¸ã«ããèªè¨¼ã¯ä»¥ä¸ã®ãããªã¡ãªãããããã¾ãã
- ãã¹ã¯ã¼ãã«å¯¾ããæ»æã«å¯¾ãã¦å®å
¨
å ¨ã¦ã®å¾æ¥å¡ã®ãã¹ã¯ã¼ããé©åã«ç®¡çããã®ã¯å¤ãã®ä¼ç¤¾ã«ã¨ã£ã¦å°é£ã§ããä¸é¨ã®å¾æ¥å¡ãå¼±ããããã¹ã¯ã¼ããæµåºãããã¹ã¯ã¼ãã使ç¨ãã¦ãã¾ããªã¹ã¯ã常ã«ããã¾ããã¯ã©ã¤ã¢ã³ã証ææ¸ãå©ç¨ãããã¨ã§ããã¹ã¯ã¼ããç ´ããã¦ãã¾ã£ã¦ããå ´åã§ãæ»æè ã«ãããã°ã¤ã³ãé²ããã¨ãã§ãã¾ãã - ãã£ãã·ã³ã°æ»æã«å¯¾ãã¦å®å
¨
æ»æè ã cybozu.com ã«é ·ä¼¼ãããµã¤ããä½ããã¨ã³ãã¦ã¼ã¶ã¼ããã®ãµã¤ãã«ãã¹ã¯ã¼ããªã©ãå ¥åãã¦ãã°ã¤ã³ãã¦ãã¾ã£ãã¨ãã¾ãããã®ãããªå ´åã§ã TLS ã®ç§å¯éµã¯æ±ºãã¦éä¿¡ç¸æã«éä¿¡ãããªããããçããã¨ãã§ãã¾ãã1ããã£ã¦æ»æè 㯠cybozu.com ã«ãã°ã¤ã³ã§ãã¾ããã
ã¯ã©ã¤ã¢ã³ã証ææ¸ã使ã£ãèªè¨¼ã¯ä»¥ä¸ã®ãããªæé ã§è¡ããã¾ãã
- ãã©ã¤ãã¼ãCAãæ§ç¯ãã
- æ°è¦ã®è¨¼ææ¸ãçºè¡ãããã®ãã©ã¤ãã¼ãCAã§ç½²åãã
- ãã®è¨¼ææ¸ãã¯ã©ã¤ã¢ã³ãã«é å¸ãã
- ã¯ã©ã¤ã¢ã³ã㯠cybozu.com ã«ã¢ã¯ã»ã¹ããéã«ãã®è¨¼ææ¸ãæåºãã
- ãµã¼ãã¼ã¯ãã©ã¤ãã¼ãCAã®è¨¼ææ¸ã使ã£ã¦ãã¯ã©ã¤ã¢ã³ãããæåºããã証ææ¸ãæ¤è¨¼ãã
éè¦ãªã®ã¯5çªã§ããµã¼ãã¼ãã¯ã©ã¤ã¢ã³ã証ææ¸ãèªè¨¼ããããã«ã¯ããã®è¨¼ææ¸ã«ç½²åããCAã®è¨¼ææ¸ãæã£ã¦ããå¿ è¦ãããã¨ãããã¨ã§ãã ã§ã¯ãµã¼ãã¼ã¯ã©ããã£ã¦CA証ææ¸ãæã«å ¥ããã°ããã®ã§ããããï¼
ãã©ã¤ãã¼ãCAãä¸åãããªãå ´åãè¤æ°ããã¨ãã¦ãå°éã§å¤åããªãå ´åã話ã¯ç°¡åã§ãã åã«ãµã¼ãã¼ã«ãã¡ã¤ã«ã¨ãã¦å梱ãã¦ããã°ããã®ã§ãã
ããã Skylab ã®å ´åã話ã¯è¤éã§ããCA ã大éã«ããããããåçã«å¢ãããæ¸ã£ããããããã§ãã cybozu.com ã«ããã¦ã顧客ã Skylab ãå¥ç´ããã¨ãã®é¡§å®¢å°ç¨ã®ãã©ã¤ãã¼ãCAãä½æãããããã¦ãã®ãµã¼ãã¹ã解ç´ããã¨ãã®CAã¯åé¤ãããä»çµã¿ã«ãªã£ã¦ãã¾ãã ãããã£ã¦ãSkylab ãæä¾ããããã«ã¯æ¤è¨¼ã«ä½¿ã CA ã®åçãªè¿½å ã»åé¤ãæ±ãããã¾ãã ãã®ãããªæ©è½ã nginx ã¯æä¾ãã¦ããªããããæã ã¯é常ã«è¦å´ãã¦ãã®æ©è½ãå®è£ ãã¾ãã2ã
ã¤ã³ã¿ã¼ã³ã®ç®æ¨ã¯ãSkylab ã Pingora ã®ä¸ã§ã¯ãªã¼ã³ã«å®è£ ãããã¨ã§ãã
Step1. ã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ãã Pingora ãµã¼ãã¼ãä½ã
æåã®ç®æ¨ã¯ãã¨ããããã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ãããã㪠Pingora ãµã¼ãã¼ãä½ããã¨ã§ãã åç㪠CA ã®ãã¼ãã¯ã¾ãå¾ã§èãããã¨ã«ãã¾ãã
ããã§è¡æã®äºå®ã¯å¤æããã®ã§ãããå°ãªãã¨ããã¼ã¸ã§ã³0.3ã®æç¹ã§ã¯ Pingora ã¯ã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ããæ©è½ãæã£ã¦ãã¾ããã§ãã (mTLS ã®ã¯ã©ã¤ã¢ã³ãå´ã«ãªãæ©è½ã¯ããã®ã§ããããµã¼ãã¼å´ã«ãªãæ©è½ã¯ãªãããã§ãã)ã
ãããªãè©°ãã ãã¨æã£ãã®ã§ãããå®ã¯å¤§ä¸å¤«ã§ãã Pingora 㯠OpenSSL ã®æ§é ä½ãç´æ¥è§¦ãããã®ã¤ã³ã¿ã¼ãã§ã¤ã¹ããããããèªåã§ã¯ã©ã¤ã¢ã³ã証ææ¸ã®è¦æ±ãæå¹åãã¦ãã¾ãã°ããã®ã§ãã
å®è£ ããã³ã¼ã(ã®æç²)ã¯ä»¥ä¸ã®ããã«ãªãã¾ãã ã³ã¼ãå ã®ã³ã¡ã³ãã¯èª¬æã®ããã«è¿½å ãããã®ã§ãã
// App ã¯åºå®ã®ã¬ã¹ãã³ã¹ãè¿ãã ãã®ãµã¼ãã¹ // Step1 ã§ã¯ãã¾ãéè¦ã§ã¯ãªãã®ã§ã詳細ã¯çç¥ãã struct App; #[async_trait] impl ServeHttp for App { async fn response(&self, _http_session: &mut ServerSession) -> Response<Vec<u8>> { ... } } // DynamicCert 㯠TLS ãã³ãã·ã§ã¤ã¯æã«å¼ã°ããã³ã¼ã«ãã㯠// Step1 ã§ã¯ãã¾ãéè¦ã§ã¯ãªãã®ã§ã詳細ã¯çç¥ãã struct DynamicCert { ... } #[async_trait] impl pingora::listeners::TlsAccept for DynamicCert { async fn certificate_callback(&self, ssl: &mut pingora::tls::ssl::SslRef) { ... } } fn main() -> Result<()> { let mut server = Server::new(None)?; server.bootstrap(); let mut app = Service::new("Saying hello service".to_string(), App); let dynamic_cert = DynamicCert::new("_wildcard.example.com.pem", "_wildcard.example.com-key.pem")?; // â ã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ããããã«è¨å® let mut tls_settings = pingora::listeners::TlsSettings::with_callbacks(dynamic_cert)?; tls_settings.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT); tls_settings.set_ca_file("client-ca.pem").unwrap(); tls_settings.set_client_ca_list(X509Name::load_client_ca_file("client-ca.pem").unwrap()); app.add_tls_with_settings("0.0.0.0:8080", None, tls_settings); server.add_service(app); server.run_forever(); Ok(()) }
éè¦ãªã®ã¯â ãä»ããé¨åã§ããããã§ã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ããããã«è¨å®ãã¦ãã¾ãã TlsSettings ã¯ãã®åã®éã TLS ã®è¨å®ãè¡ãããã®æ§é ä½ã§ãããããã OpenSSL ã®é¢æ°ãèãã©ããããã¤ã³ã¿ã¼ãã§ã¤ã¹ãæä¾ãã¦ããããããéãã¦ã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ããããã«è¨å®ãããã¨ãã§ãã¾ãã
ä¾ãã° TlsSettings::set_verify
㯠SSL_CTX_set_verify ãã©ããããã¡ã½ããã§ãå¼æ°ã«æ¸¡ããããããã©ã°ã«ãã£ã¦è¨¼ææ¸ã®æ¤è¨¼æ¹æ³ãè¨å®ãã¾ãã
ä»å㯠PEER (ã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ãã) 㨠FAIL_IF_NO_PEER_CERT (ã¯ã©ã¤ã¢ã³ã証ææ¸ãæåºãããªãã£ãå ´åã«ã¨ã©ã¼ã«ãã) ãæå®ãã¦ãã¾ãã
ã¾ããset_ca_file
㨠set_client_ca_list
ããããã SSL_CTX_load_verify_locations 㨠SSL_CTX_set_client_CA_list ã«å¯¾å¿ãã¦ãã¾ãã
åè
ãã¯ã©ã¤ã¢ã³ã証ææ¸ãæ¤è¨¼ããããã® CA ãè¨å®ãããã®ã§ãå¾è
ãã¯ã©ã¤ã¢ã³ãã¸éã CA ãªã¹ããè¨å®ãããã®ã§ãã
ããã§æ¥ç¶æã«ã¯ã©ã¤ã¢ã³ã証ææ¸ãè¦æ±ãããã㪠Pingora ãµã¼ãã¼ãã§ãã¾ããã
Step2. 顧客ãã¨ã«ç°ãªãCAã§æ¤è¨¼ãã
次ã«ã顧客ãã¨ã«ç°ãªã CA ã使ã£ã¦ã¯ã©ã¤ã¢ã³ã証ææ¸ãæ¤è¨¼ããããã«ãã¾ãã
ããã®å®è£ ã«ã¯ä¸ã®ä¾ã§ãã¡ãã£ã¨åºã¦ãããã³ãã·ã§ã¤ã¯æã«å¼ã°ããã³ã¼ã«ããã¯ã使ãã¾ãã ãããããã®ã³ã¼ã«ããã¯ã«ã¯ç½ ãããã¾ãã å¼ã°ããã¿ã¤ãã³ã°ã ClientHello 㨠ServerHello ã®éãªã®ã§ãã
ä¸æ¹ãã¯ã©ã¤ã¢ã³ã証ææ¸ã¯ ServerHello ããå¾ã«éããã¦ãã¾ãã ã¤ã¾ãã³ã¼ã«ããã¯é¢æ°ãå¼ã°ããã¿ã¤ãã³ã°ã§ã¯ã©ã¤ã¢ã³ã証ææ¸ãèªããã¨ã¯ä¸å¯è½ãªã®ã§ãã
Skylab ã§ã¯ã¯ã©ã¤ã¢ã³ã証ææ¸ã®æ¸ããã¦ãã顧客IDããã¨ã« CA ããã¼ããã¦ãã¾ããã Pingora ã§ã¯åæ§ã®å®è£ ãä¸å¯è½ã§ãã
ãã°ãããã®åé¡ã§å°ã£ã¦ãã¾ããããããèãã㨠ClientHello ã«ã¯ SNI ãå«ã¾ãã¦ãããããã使ã£ã¦é¡§å®¢ãç¹å®ããã°ãããã¨ã«æ°ä»ãããªãã¨ãå®è£ ãããã¨ãã§ãã¾ããã
SNI ã¨ããã®ã¯ãTLS ã®ãã³ãã·ã§ã¤ã¯ã®éã«ã¯ã©ã¤ã¢ã³ãããµã¼ãã¼ã«å¯¾ãã¦æ示ãããã¹ãåã®ãã¨ã§ãã
ä¾ãã°ãã¯ã©ã¤ã¢ã³ãã https://example.com/
ã«ã¢ã¯ã»ã¹ããå ´åãSNI ã«ã¯ example.com
ãå
¥ãã¾ãã
cybozu.com ã§ã¯é¡§å®¢ãã¨ã«ç°ãªããµããã¡ã¤ã³ã§ãµã¼ãã¹ãæä¾ãã¦ãããããSNI ãè¦ãã ãã§ã©ã® CA ã使ã£ã¦ã¯ã©ã¤ã¢ã³ã証ææ¸ãæ¤è¨¼ããã°ãããããããããã§ãã
以ä¸ã®ã³ã¼ãã¯ãã®å®è£ ã§ããå°ãé·ãã®ã§ã³ã¼ã«ããã¯é¢æ°ã®é¨åã®ã¿æãåºãã¦ãã¾ãã
#[async_trait] impl pingora::listeners::TlsAccept for DynamicCert { async fn certificate_callback(&self, ssl: &mut pingora::tls::ssl::SslRef) { use pingora::tls::ext; // ãµã¼ãã¼è¨¼ææ¸ãè¨å® // â» unwrap ã使ã£ã¦ããããå¾ã§ã¾ã¨ããªã¨ã©ã¼ãã³ããªã³ã°ã追å ãã ext::ssl_use_certificate(ssl, &self.cert).unwrap(); ext::ssl_use_private_key(ssl, &self.key).unwrap(); // SNIã«å¿ãã¦ç°ãªã証ææ¸ã使ã // ããã§ã¯ hoge.example.com 㨠fuga.example.com ã®ã¿ããµãã¼ããã if let Some(sni) = ssl.servername(NameType::HOST_NAME) { match sni { "hoge.example.com" => load_certificate(ssl, "hoge-client-ca.pem").unwrap(), "fuga.example.com" => load_certificate(ssl, "fuga-client-ca.pem").unwrap(), other => println!("SNI {} is unknown", other), } } } } // æå®ããããã¡ã¤ã«ãã証ææ¸ããã¼ããã¦æ¤è¨¼ç¨ã® CA ã¨ãã¦è¨å®ãã // â» ã¨ã©ã¼ãã³ããªã³ã°ã®ã³ã¼ã㯠... ã§çç¥ãã¦ãã fn load_certificate(ssl: &mut SslRef, filename: &str) -> Result<()> { // ãã¡ã¤ã«ãã X509 証ææ¸ããã¼ã let certificate = fs::read(filename).map_err(|e| ...)?; let certificate = X509::from_pem(&certificate).map_err(|e| ...)?; let mut ca_stack = Stack::new().map_err(|e| ...)?; let ca_name = certificate.subject_name().to_owned().map_err(...)?; ca_stack.push(ca_name).map_err(...)?; // ã¯ã©ã¤ã¢ã³ãã«æ¸¡ã CA ãªã¹ããè¨å® ssl.set_client_ca_list(ca_stack); let mut builder = X509StoreBuilder::new().map_err(...)?; builder.add_cert(certificate).map_err(...)?; let certificate_store = builder.build(); // ã¯ã©ã¤ã¢ã³ã証ææ¸ã®æ¤è¨¼ã«ä½¿ã CA ãè¨å® ssl.set_verify_cert_store(certificate_store).map_err(...)?; Ok(()) }
ãã®ã³ã¼ã«ããã¯ã«ãã£ã¦ã¯ã©ã¤ã¢ã³ã証ææ¸ã®æ¤è¨¼ã«å©ç¨ãã CA ãè¨å®ããã¾ãã ãã³ãã·ã§ã¤ã¯ãé²ãã§ã¯ã©ã¤ã¢ã³ã証ææ¸ãéããã¦ããããããã§æå®ããã CA ã«ãã£ã¦æ¤è¨¼ãè¡ãããããã«ãªãã¾ããã
Step3. å¤é¨ã®ãµã¼ãã¼ãã CA ããã§ãããã
Step2 ã®ã³ã¼ãã§ã¯ãã¼ã«ã«ãã¡ã¤ã«ã·ã¹ãã ãã証ææ¸ããã¼ããã¦ãã¾ããããåçã«å¢æ¸ãã証ææ¸ããã¼ããã©ã³ãµã®ãã¡ã¤ã«ã·ã¹ãã ã«åæããã®ã¯å®éã®éç¨ã§ã¯å°é£ã§ãã ããã§ãä»ã®ãµã¼ãã¼ãã HTTP çµç±ã§è¨¼ææ¸ãåå¾ãããã¨ã«ãã¾ãã
ããã¯ç°¡åã«å®è£
ã§ãã¾ãã
åã«ãfs::read
ãã¦ããã¨ããã reqwest::get
ã«ç½®ãæããã ãã§ãã
Step3 㯠Pingora ã«ããã¦ã¯äºç´°ãªã¹ãããã§ãããå®ã¯ nginx ã«ããã¦ã¯å®ç¾ãé£ãã課é¡ã§ããã nginx ã§ã¯ OpenSSL ã®ã³ã¼ã«ããã¯ã®ä¸ã§éåæãªé¢æ°ãå¼ã¶ãã¨ãã§ããªããããä»ã®ãµã¼ãã¼ã¨éä¿¡ãããã¨ãã§ãã¾ããã ãããã£ã¦ããµã¤ãã¦ãºã®ç¾å¨ã®ãã¼ããã©ã³ãµã¯ãã¼ã«ã«ãã¡ã¤ã«ã·ã¹ãã ã«å ¨ã¦ã®CA証ææ¸ãæã£ã¦ãããã¨ãä½åãªãããã¦ãã¾ãã ã¤ã¾ãããã¼ããã©ã³ãµãã¹ãã¼ããã«ã«ãªã£ã¦ãã¾ãã
ç¾å¨æã ã¯ã¤ã³ãã©ã Kubernetes ã®ä¸ã«åæ§ç¯ãããã¨ãã¦ãã¾ãã ãã¼ããã©ã³ãµããã¤ã Kubernetes ã®ä¸ã«æã£ã¦ãããªããã°ãªãã¾ããã ãããã¹ãã¼ããã«ãªãµã¼ãã¼ã¯ Kubernetes ã®ä¸ã§åããã®ãã¨ã¦ã大å¤ã§ãã ãã£ã¦ãã¼ããã©ã³ãµã¯ã¹ãã¼ãã¬ã¹ã«ãã¦ã証ææ¸ã®ãããªå¯å¤ãªãã¼ã¿ã¯æ¢åã®(é å¼µã£ã¦éç¨ããã¦ãã)ãã¼ã¿ã¹ãã¢ããåå¾ããããã«ãããã£ãããã§ãã ãã®ãããªæå³ã§ãStep3 ã¯ä»åã® PoC ã®ãã¼ã¨ãªããã¤ã«ã¹ãã¼ã³ãªã®ã§ãã
Step4. ã¦ã¼ã¶ã¼ãã¬ã³ããªã¼ãªã¨ã©ã¼ãè¿ã
Step3 ã¾ã§ã®å®è£ ã§ã¯ã¨ã©ã¼ãã³ããªã³ã°ã«åé¡ãããã¾ããã
- ã¯ã©ã¤ã¢ã³ã証ææ¸ãæåºãããªãã£ãå ´åããæåºããã証ææ¸ãæ¤è¨¼ã«å¤±æããå ´åããµã¼ãã¼ã¯ã¬ã¹ãã³ã¹ãè¿ããã«å³åº§ã«æ¥ç¶ãåæãã¦ãã¾ãã¾ãã ãã®ã¨ããã©ã¦ã¶ã¯çã£ç½ãªç»é¢ã表示ããã ãã§ãã¦ã¼ã¶ã¼ã«ã¯ä½ãèµ·ãã£ãã®ããããã¾ããã
- SSLãã³ãã·ã§ã¤ã¯ã®ã³ã¼ã«ããã¯é¢æ°ã®ä¸ã§ã¨ã©ã¼ãçºçããå ´åãunwrap ã§ãããã¯ãã¦ãã¾ãã¾ãã ãã®å ´åãã¦ã¼ã¶ã¼ã«ããããããã¨ã©ã¼ã¡ãã»ã¼ã¸ã表示ãããã¨ãã§ãã¾ããã
ãµã¼ãã¹ã¨ãã¦æä¾ããã«ã¯ãã¨ã©ã¼ãçºçããå ´åã«ã¦ã¼ã¶ã¼ã«ããããããã¨ã©ã¼ã¡ãã»ã¼ã¸ã表示ãããã¨ãéè¦ã§ãã ãã£ã¦ãStep4 ã§ã¯ã¨ã©ã¼ãã³ããªã³ã°ãæ¹åããã¦ã¼ã¶ã¼ã«ä½ãèµ·ãã£ãã®ããã¨ã©ã¼ã¡ãã»ã¼ã¸ã¨ãã¦è¿ããããã«ãã¾ãã
ã¾ããTLS ã® verify ã®è¨å®ãå¤æ´ãã¾ãã
// ä»ã¾ã§è¡ã£ã¦ãã // tls_settings.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT); // ã®ä»£ããã«ä»¥ä¸ã®ããã«è¨å®ããã tls_settings.set_verify_callback(SslVerifyMode::PEER, |_, _| true);
set_verify_callback
㯠set_verify
ã¨ä¼¼ãã¡ã½ããã§ãããã¢ã¼ãã®è¨å®ã«å ãã¦è¨¼ææ¸æ¤è¨¼æã®ã³ã¼ã«ããã¯é¢æ°ãæå®ãããã¨ãã§ãã¾ãã
ãã㯠OpenSSL ãã¯ã©ã¤ã¢ã³ã証ææ¸ãæ¤è¨¼ããç´å¾ã«å¼ã°ããã³ã¼ã«ããã¯ã§ãã¯ã©ã¤ã¢ã³ã証ææ¸ã®æ¤è¨¼çµæããµã¾ããä¸ã§æ¥ç¶ãåãå
¥ããã®ãæå¦ããã®ãã決å®ãããã¨ãã§ãã¾ãã
æã
㯠HTTP ã®ã¬ã¤ã¤ã¼ã§ã¨ã©ã¼ã¡ãã»ã¼ã¸ãè¿ãããã¨æã£ã¦ããã®ã§ããã®ã³ã¼ã«ããã¯é¢æ°ã¯å¸¸ã« true
ãè¿ãããã«ãã¦ããã¾ãã
ãã®è¨å®ã«ããã証ææ¸ã®æ¤è¨¼ã«å¤±æããå ´åãã証ææ¸ãã¯ã©ã¤ã¢ã³ãããæ示ãããªãã£ãå ´åã«ããµã¼ãã¼ã¯ TCP ã³ãã¯ã·ã§ã³ãåæãããHTTPãã³ãã©ã¾ã§å¦çãå°éããããã«ãªãã¾ããã
次ã«ãããã¨ã¯ãHTTPãã³ãã©ã®ä¸ã§è¨¼ææ¸ã¨ã©ã¼ããã®ä»ã®ã¨ã©ã¼ããã£ããããã¦ã¼ã¶ã¼ã«ã¨ã©ã¼ã¡ãã»ã¼ã¸ãè¿ããã¨ã§ãã
ã¨ããã§ãcertificate_callback
ã¯æ»ãå¤ãæã¡ã¾ããã§ããããã£ããã©ããã£ã¦ã¨ã©ã¼ã HTTP ãã³ãã©ã«æ¸¡ãã°ããã®ã§ããããï¼
å®ã¯ OpenSSL ã«ã¯ä»»æã®ãã¼ã¿ã Ssl æ§é ä½ã«æ ¼ç´ããããã®æ©è½ãããã¾ãã
ããã使ããã¨ã§ certificate_callback
ã§çºçããã¨ã©ã¼ããã³ãã©ã¾ã§æ¸¡ããã¨ãã§ãã¾ãã
ã¾ã㯠Ssl::new_ex_index
ãå¼ãã§ã¨ã©ã¼ãæ ¼ç´ããããã®ã¤ã³ããã¯ã¹ãä½æãã¦ããã¾ãã
// ãµã¼ãã¼èµ·åæã« new_ex_index ãå¼ãã§ã¨ã©ã¼ãæ ¼ç´ããããã®ã¤ã³ããã¯ã¹ãä½æãã¦ãã let error_index = Ssl::new_ex_index()?;
ããã¦ãcertificate_callback
ã§ã¨ã©ã¼ãçºçããå ´åã¯ããã®ã¨ã©ã¼ã Ssl æ§é ä½ã«æ ¼ç´ãã¾ãã
async fn certificate_callback(&self, ssl: &mut SslRef) { ... // ã³ã¼ã«ããã¯é¢æ°å ã§ã¨ã©ã¼ãçºçããå ´åã¯ãã¨ã©ã¼ã ssl ã«æ ¼ç´ãã if let Err(e) = load_certificate(ssl, &pem) { ssl.set_ex_data(self.error_index, *e); return; } ... }
æå¾ã«ãHTTP ãã³ãã©ã®ä¸ã§ã¨ã©ã¼ãåãåºãã¦ãã¦ã¼ã¶ã¼ã«ã¨ã©ã¼ã¡ãã»ã¼ã¸ãè¿ãã¾ãã
ã¾ããã¯ã©ã¤ã¢ã³ã証ææ¸ãæ示ããã¦ãããã©ãã㯠ssl.peer_certificate()
ã§ãæ¤è¨¼ã«å¤±æãããã©ãã㯠ssl.verify_result()
ã§ããããå¤å®ã§ãã¾ãã
#[async_trait] impl ServeHttp for App { async fn response(&self, http_session: &mut ServerSession) -> Response<Vec<u8>> { let Some(stream) = http_session.stream() else { return ... }; let Some(ssl) = stream.get_ssl() else { return ... }; // ã¯ã©ã¤ã¢ã³ã証ææ¸ãã¯ã©ã¤ã¢ã³ãããæ示ããã¦ããªãå ´åãFORBIDDEN ãè¿ã if ssl.peer_certificate().is_none() { return build_text_response(StatusCode::FORBIDDEN, "Client certificate not found"); } // ãã³ãã·ã§ã¤ã¯å ã§çºçããã¨ã©ã¼ã ssl ããåå¾ããããã¨ã©ã¼ãããã° FORBIDDEN ãè¿ã if let Some(e) = ssl.ex_data(self.error_index) { return build_text_response(StatusCode::FORBIDDEN, &e.to_string()); } // ã¯ã©ã¤ã¢ã³ã証ææ¸ã®æ¤è¨¼ã«å¤±æããå ´åãFORBIDDEN ãè¿ã if ssl.verify_result() != X509VerifyResult::OK { return build_text_response( StatusCode::FORBIDDEN, "Failed to verify a client certificate", ); } // ã¯ã©ã¤ã¢ã³ã証ææ¸ã®æ¤è¨¼ã«æåããã®ã§ãæ£å¸¸ãªã¬ã¹ãã³ã¹ãè¿ã ... } }
以ä¸ã§ãã¯ã©ã¤ã¢ã³ã証ææ¸ã®æ¤è¨¼ã«å¤±æããå ´åãã¯ã©ã¤ã¢ã³ã証ææ¸ãæ示ããã¦ããªãã£ãå ´åãããã¦ã³ã¼ã«ããã¯é¢æ°å ã§å é¨ã¨ã©ã¼ãçºçããå ´åã«ãã¦ã¼ã¶ã¼ã«ã¨ã©ã¼ã¡ãã»ã¼ã¸ãè¿ããã¨ãã§ããããã«ãªãã¾ããã
ãã®ã³ã¼ãã«ã¯ã²ã¨ã¤å¾®å¦ãªç¹ãããã¾ãã
ãã㯠ServerSession
ãã Ssl
ãåãåºããã¨ãã§ããã®ã¯ HTTP/1.1 ã®å ´åã®ã¿ã§ãHTTP/2 ã®å ´åã¯åãåºããªãã¨ãããã¨ã§ãã
幸ã Skylab ã¯ãããã HTTP/2 ã§ã®å©ç¨ãæ³å®ãã¦ããªãããããã®åé¡ã¯ä»åã® PoC ã«ããã¦ã¯åé¡ã«ãªãã¾ããã§ãããããããã¸ã㯠Pingora ã®å°æ¥ã«æå¾
ãããã¨ããã§ãã
å®æ
以ä¸ãããPingora ã使ã£ã¦ Skylab æ©è½ãå®è£ ãããã¨ãã§ãã¾ããã 説æã¯çãã¾ãããããã® PoC ã¯ãªãã¼ã¹ãããã·ã®æ©è½ãæã£ã¦ãããSkylab ã®èªè¨¼ãè¡ã£ãä¸ã§ä»»æã®ããã¯ã¨ã³ããµã¼ãã¼ã«ãªã¯ã¨ã¹ãã転éãããã¨ãã§ãã¾ãã ã¤ã¾ããä»»æã®ããã¯ã¨ã³ãã« Skylab ã®èªè¨¼ã追å ã§ãããããªããã«ã¦ã§ã¢ã«ãªã£ã¦ãã¾ãã
ã³ã¼ãè¡æ°ã¯ 300 è¡ç¨åº¦ã§ãããã¯ã©ã¤ã¢ã³ã証ææ¸ä»¥å¤ã®é¨åã Pingora ã«ã¾ããã¦ãããããç°¡æ½ã§ããããããã³ã¼ãã«ãªã£ãã¨æãã¾ãã
ãããã«
ã¤ã³ã¿ã¼ã³ã·ãããå§ã¾ã£ãã¿ã¤ãã³ã°ã§ã¯ãã¡ã³ã¿ã¼ã Pingora ã«é¢ãã¦ã¯ Pingora å ¥é ã®ç¯ã«æ¸ããç¨åº¦ã®ç¥èãããªããSkylab ã®æ©è½ãæ¬å½ã«å®è£ ã§ããã®ãåãããªãç¶æ ã§ããã ãã®ã»ã¼ã¼ãã¨ãè¨ããç¶æ ã§è¦éããã«å®è£ ãé²ãã¦ããã ããæçµçã«ã¯ PoC ã®å®è£ ãå®äºãããã¨ãã§ãã¾ããã Step1ï½Step4 ã§ç´¹ä»ããã³ã¼ãã¯è¦éããã®æ¸ããã³ã¼ããã»ã¼ãã®ã¾ã¾å¼ç¨ãããã®ã§ãï¼ãã ãã³ã¡ã³ãã¯ãã®è¨äºã®ããã«ç§ã追å ãã¾ããï¼ã ä»åã®èª²é¡ã¯ Rust 㨠OpenSSL ã®ç¥èãè¦æ±ããé£æ度ã®é«ããã®ã ã£ãã¨æãã¾ããã大ããè©°ã¾ããã¨ãªãã¹ã ã¼ãºã«å®æã¾ã§é²ã¿ãã¡ã³ã¿ã¼ã¨ãã¦ãé©ãã¦ãã¾ãã ãããã¨ããããã¾ããã
- TLS ã§ã¯ã (1) ã¯ã©ã¤ã¢ã³ãã¯ç§å¯éµã使ã£ã¦ã¡ãã»ã¼ã¸ã«ç½²åããå ¬ééµã¨ãã®ç½²åããµã¼ãã¼ã«éä¿¡ãã (2) ãµã¼ãã¼ã¯éããã¦ããå ¬ééµã使ã£ã¦ç½²åãæ£ãããæ¤è¨¼ãã ã¨ããæé ã§ã¯ã©ã¤ã¢ã³ããæ£ãããã¨ã確ããã¦ãã¾ããå®ã¯ããµã¼ãã¼ã«ã¯ã©ã¤ã¢ã³ã証ææ¸ãéä¿¡ãããã¨è¨ã£ãã¨ãã®ãã¯ã©ã¤ã¢ã³ã証ææ¸ãã®ä¸èº«ã¯ãã®å ¬ééµï¼ã¨ããã¤ãã®ã¡ã¿ãã¼ã¿ï¼ãªã®ã§ããç§å¯éµããµã¼ãã¼ã«éä¿¡ããªãã¦ããµã¼ãã¼ã¯ç½²åãæ£ãããæ¤è¨¼ã§ããã¨ããã®ããã¤ã³ãã§ãã↩
- nginx ã«ããã Skylab ã®å®è£ ã¯ãã®çºè¡¨ã®43ï½47ãã¼ã¸ãåç §ãã¦ãã ãã: https://www.slideshare.net/slideshow/devsummit2015/44905601↩