본문 바로가기
IT/오픈소스

네티(Netty) 기본정리2

by 모띠 2022. 2. 22.

 

 

본 포스팅은 2편으로 구성되어 있습니다.

네티(Netty) 기본정리

네티(Netty) 기본정리2

 

네티 채널과 보안

네트워크 데이터 캡쳐

네트워크로 전송된 데이터를 가로채는것을 네트워크 데이터 캡쳐라고 부른다. 방법은 크게 3가지

  1. tcp dump계열의 네트워크 분석 도구 사용 - 와이어샤크(윈도우), tcpdump(리눅스), 이더리얼
  2. 브라우저가 제공하는 부가기능이나 플러그인 - 파이어폭스의 파이어버그, 크롬의 개발자도구
  3. http 및 https와 같이 특정한 프로토콜에 해당하는 데이터만을 수집하는 도구 - 피들러

http는 피들러를 이용하면 데이터를 가로챌 수 있다. tcp도 와이어샤크같은걸 이용해서 가로챌 수 있다.

(다만 테스트해보려면 텔넷서버를 만들고 텔넷을 쏴야하는데, 별도의 운영체제에서 해야한다. 윈도우로만 놓고쏘면 루프백어댑터에대한 패킷을 캡쳐하지못함)

네트워크 보안

네트워크를 보안하게위한 대비책은 크게 2가지이다.

  1. 전송되는 데이터중에 중요한 부분만 암호화 - 필요한 데이터만 암호화하므로 시스템 리소스를 적게사용하지만 유추한 단점, 복화화 키를 안전하게 공유하는 단점
  2. 전송되는 모든 데이터를 암호화하는 방법 (채널, 전송계층 암호화, ssl, tls, vpn)

TLS

TLS는 SSL을 기반으로 한 보안 소켓 규격으로서 네트워크로 전송되는 데이터를 암호화하여 보호하는기술. TLS이 표준명칭이지만 SSL는 용어가 더 대중적.

TLS은 비밀키와 공개키방식을 동시에 사용하는데, 핸드쉐이크 방식으로 비밀키를 공개키 알고리즘으로 암호화해서 전송하고 이 공유된 비밀키로 대칭키 암호화알고리즘을 이용하여 서로 통신

네티 서버에 TLS프로토콜을 적용하려면 공개키 암호화에서 사용되는 인증서가 필요. (pem 파일)

SSL 인증서가 필요한데, JDK의 keytool(JKS)같은 인증서 관리도구나 Openssl프로그램을 사용하여 만들 수 있음. (윈도우는 설치해야하고 리눅스는 기본적으로 깔려있음)

  1. RSA 비밀키생성
// 공개키 알고리즘 RSA로 RSA비밀키를 생성하고 그 비밀키를 AES알고리즘으로 암호화
// privatekey.pem 파일이 RSA비밀키임
openssl genrsa -aes256 -out privatekey.pem 2048
  1. RSA 비밀키를 이용하여 CSR 생성
// 공개키는 별도로 생성하는게 맞는데, 어차피 인증서 서명요청하기위한 CSR을 생성할때 자동으로 공개키가 포함되어 생성되므로 CSR만 만들면됨.
// CSR파일은 파일을 생성한 기관의 공개키와 회사의 정보가 포함되어있음
openssl req -new -key privatekey.pem -out netty.csr
  1. 자체 서명
// CSR파일을 상위 인증기관으로 전송하여 인증기관의 비밀키로 전자서명을 받으면 인증서가 완료됨.
// 다만 실제로 서명을 받기위해서는 비용을 지불해야함.. 그래서 테스트를 위해서 인증기관의 비밀키가 아니라 나의 비밀키로 자체서명
openssl x509 -in netty.csr -out netty.crt -req -signkey privatekey.pem -days 365
  1. 인증서 검증
// 인증서를 발급한 기관의 정보 및 유효기간 확인
openssl x509 -noout -text -in ./netty.crt

// 개인키생성에 사용된 비트수확인
openssl rsa -noout -text -in privatekey.pem
  1. ASN.1 → PKCS#8 형식으로 변환 (선택)
// privatekey.pem.org로 원본파일을 복사해놓고 이파일을 이용하여 pkcs형식으로 변환
openssl pkcs8 -topk8 -inform PEM -outform PEM -in privatekey.pem.org -out privatekey.pem

네티채널에 보안적용

네티에 SSL/TLS연결을 적용하는 방법은 매우 간단하다.

생성한 인증서와 개인키로 네티의 SslContext인스턴스를 생성하고 채널 파이프라인 가장 앞쪽에 SslContext로부터 생성한 SslHandler를 등록해주면된다.

...
File certChainFile = new File("netty.crt");
File keyFile = new File("privatekey.pem");

// 생성한 인증서와 개인키로 네티의 SslContext인스턴스를 생성
SslContext sslCtx = SslContext.newServerContext(certChainFile, keyFile, "비밀번호");
...

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(...)
 .childHandler(new HttpSnoopServerInitializer(**sslCtx**));
...

class HttpSnoopServerInitializer extends ChannelInitializer<SocektChanne>{
	private final SslContext sslCtx;

	public HttpSnoopServerInitializer (SslContext sslCtx){
		this.sslCtx = sslCtx;
	}

	@Override
	public void initChannel(SocketChannel ch){
		ChannelPipeline p = ch.pipeline();
		if (sslCtx != null){
		// 채널 파이프라인 가장 앞쪽에 SslContext로부터 생성한 SslHandler를 등록
		**p.addLast(sslCtx.newHandler(ch.alloc()));**
		}
		p.addLast(...)
	}
}

https://localhost:443에 접속하면 신뢰할수없는 보안인증서라며 경고창이 뜨게된다. (정상)

이것은 브라우저가 접속한 서버가 전송한 인증서의 전자서명이 신뢰된 인증기관(root CA)가 발행한것이 아니기때문이다 (우리가 자신의 개인키로 서명했음)

 


@Sharable

private static final TelnetServerHandler SERVER_HANDLER = new TelnetServerHandler();
...

.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						public void initChannel(SocketChannel ch) {
							ChannelPipeline p = ch.pipeline();
							p.addLast(new EchoServerV3FirstHandler()); // 지금까지는 파이프라인이 초기화될때 new로 새롭게 생성되었다.
							p.addLast(SERVER_HANDLER); // 하지만 이런식으로 하나의 데이터 핸들러 객체를 모든 채널파이프라인이 공유하도록 할 수 있음.
																				// 단 @Sharable 어노테이션으로 공유가능한 코덱이라는것을 나타내야함
						}
					});

@Sharable // 스레드 safe하게 소스를 짜야함.
public class TelnetServerHandler extends SimpleChannelInboundHandler<String> { ... }

JUnit

네티의 이벤트 핸들러는 네티 프레임워크에서 발생한 이벤트를 처리하는 인터페이스다. 이벤트 핸들러를 테스트하려면 강제로 네티이벤트를 발생시켜야하며 그에따른 이벤트 루프를 설정하는등 굉장히 복잡하다. 네티는 이값은 복잡한 설정없이 이벤트 핸들러를 테스트할 수 있는 EmbeddedChannel클래스를 제공한다. 이 클래스를 사용하면 채널 파이트라인 및 이벤트 루프 설정같은 부가 작업없이 순수하게 이벤트 핸들러를 테스트할 수 있다.

네티를 사용하여 DelimiterBasedFrameDecoder 클래스를 테스트하는 예제이다.

DelimiterBasedFrameDecoder 클래스는 인바운드로 입력된 데이터를 구분자를 기준으로 잘라서 돌려주는 네티의 기본 제공 디코더다.

 

(디코더 = 인바운드 / 인코더 = 아웃바운드)

public class DelimiterBasedFrameDecoderTest {
	@Test
	public void testDecoder() {
		String writeData = "안녕하세요\\r\\n반갑습니다\\r\\n";
		String first = "안녕하세요\\r\\n";
		String second = "반갑습니다\\r\\n";

		// 최대 8192바이트 데이터를 줄바꿈 문자 기준으로 잘라서 디코딩, 2번재 인수는 디코딩된 데이터에 구분자 포함여부를 뜻함.
		DelimiterBasedFrameDecoder decoder = new DelimiterBasedFrameDecoder(8192, false, Delimiters.lineDelimiter());
		EmbeddedChannel embeddedChannel = new EmbeddedChannel(decoder); // 테스트할 핸들러 등록

		ByteBuf request = Unpooled.wrappedBuffer(writeData.getBytes());  // 문자열을 네티의 바이트버퍼로 변환
		boolean result = embeddedChannel.writeInbound(request); // 바이트버퍼를 인바운드에 기록 = 클라이언트로부터 데이터를 수신한것과 동일
		assertTrue(result);
		
		ByteBuf response = null;
		response = (ByteBuf) embeddedChannel.readInbound(); // 인바운드 데이터를 읽는다. 
		assertEquals(first, response.toString(Charset.defaultCharset())); // 안녕하세요/r/n를 돌려줌
		
		response = (ByteBuf) embeddedChannel.readInbound(); // 인바운드 데이터를 읽는다. 
		assertEquals(second, response.toString(Charset.defaultCharset())); // 반갑습니다/r/n를 돌려줌
		
		embeddedChannel.finish(); // 종료
	}
}
@Test
	public void testEncoder() {
		String writeData = "안녕하세요";
		ByteBuf request = Unpooled.wrappedBuffer(writeData.getBytes()); // 문자열을 네티의 바이트버퍼로 변환

		Base64Encoder encoder = new Base64Encoder();
		EmbeddedChannel embeddedChannel = new EmbeddedChannel(encoder); // 테스트할 핸들러 등록

		boolean result = embeddedChannel.writeOutbound(request); // 바이트버퍼를 아웃바운드에 기록 = 클라이언트로부터 데이터를 수신한것과 동일
		assertTrue(result);

		ByteBuf response = (ByteBuf) embeddedChannel.readOutbound(); // 아웃바운드 데이터를 읽는다.

		String expect = "7JWI64WV7ZWY7IS47JqU";
		assertEquals(expect, response.toString(Charset.defaultCharset())); 
		
		embeddedChannel.finish(); // 종료
	}
	
	@Test
	public void testDecoder() {
		String writeData = "7JWI64WV7ZWY7IS47JqU";
		ByteBuf request = Unpooled.wrappedBuffer(writeData.getBytes()); // 문자열을 네티의 바이트버퍼로 변환

		Base64Decoder decoder = new Base64Decoder();
		EmbeddedChannel embeddedChannel = new EmbeddedChannel(decoder); // 테스트할 핸들러 등록

		boolean result = embeddedChannel.writeInbound(request); // 바이트버퍼를 인바운드에 기록
		assertTrue(result);

		ByteBuf response = (ByteBuf) embeddedChannel.readInbound(); // 인바운드 데이터를 읽는다.

		String expect = "안녕하세요";
		assertEquals(expect, response.toString(Charset.defaultCharset())); 
		
		embeddedChannel.finish(); // 종료
	}

'IT > 오픈소스' 카테고리의 다른 글

JPA - 기본 개념  (1) 2024.01.30
카프카(Kafka) - 활용정리  (0) 2023.09.12
카프카(Kakfa) 개념정리  (1) 2023.09.11
Docker란?  (1) 2023.03.26
네티(Netty) 기본정리  (0) 2022.02.06

댓글