1. IP 필터 구현 이야기
최범균 (madvirus@madvirus.net, 트위터: @madvirus)
2. 이야기의 시작....
● 2011년 어느 날
○ 장애 신고: "고객이 웹 게임에 연결이 안 된데요!"
○ 게임 관련된 서버들 확인
■ 게임 웹 서버: 팡 팡 놀고 있음
■ DB 서버: 팡 팡 놀고 있음
■ 게임 배치 서버: 팡 팡 놀고 있음
○ 증상
■ 내부 네트워크에서 잘 연결 됨
■ 외부 네트워크에서 연결 매우 느림
○ 장애 지점
■ 더 뒤져보니.......
3. 이야기의 시작....
● 웹 방화벽이 장애 지점이었음!!
○ 컥, 웹 방화벽의 CPU 사용률이 100% 가까이 치솟음
● 원인
○ 웹 게임으로 인해 웹 트래픽 급증
○ 웹 방화벽의 차단 IP 목록의 개수가 무지 많음
○ 각 클라이언트 IP가 차단 IP인지 검사하느라 CPU가
무지 바쁘게 일하게 됨
○ 방화벽이 버벅되면서 외부에서 접근하는 모든 웹 연
결에 문제가 발생함
● 일단 문제 해소부터
○ 웹 게임에 대해 차단 규칙 제외해서 일단 급한 불은
껐으나,,, 보안에 찜찜함이 남음...
4. 그래, 만들어볼까?
● 의심나는 것
○ 클라이언트 IP가 차단 IP인지 검사할 때, 전체 차단 IP
목록/패턴을 검색하는 건 아닐까?
○ 마치 DB에서 풀(full) 스캔하는 것과 같은 상황?
● 문뜩 떠오른 생각
○ DB에 인덱스를 만들 듯, IP 목록을 인덱스 방식으로
관리하면 차단 IP인지 여부를 빠르게 확인할 수 있을
텐데.....
● 만들어볼까?
○ 생각만 하고, 2년이 지난뒤에 만들게 된 ip-filter !
5. 내용
● 트리 기반 차단/허용 IP 목록 관리/검색 구현
○ 검색 성능 비교
● SLF4J 방식 컴포넌트 구현
● 문자열 기반 설정
○ Scala Combinator Parser
7. IP 패턴을 목록으로 관리하면
1.2.3.4
1.2.3.64/26
5.6.7.*
10.20.*
...
...
...
30.*
10.30.40.51
10.30.40.52
10.30.40.51 검사
10.20.1.2 검사
3만개 목록일 경우,
평균 1.5만개 비교
6만개 목록일 경우,
평균 3만개 비교
8. IP 패턴을 트리로 표현하면
루트
1 10
2 12
3
4 128/25
13
14
20
*
30
40
51 52
10.30.40.51
10.20.1.2
1.2.3.200
3만개 목록일 경우,
최대 5레벨 깊이 탐색
6만개 목록일 경우,
최대 5레벨 깊이 탐색
9. 적용할 IP 패턴
● 1.2.3.4: 정확한 매칭
● 1.2.3.n/m: 네트워크 주소를 이용한 범위 표현
○ 예:
■ 1.2.3.64/26: 1.2.3.64~127 (01000000 ~ 01111111)
■ 1.2.3.0/26: 1.2.3.0~63 (00000000 ~ 00111111)
● 1.2.3.*: 전체 범위
○ 예
■ 1.2.3.*: 1.2.3.0~1.2.3.255
■ 1.2.*: 1.2.0.0~1.2.255.255
10. IP 패턴의 트리 표현위한 두 클래스
루트
1 10
2 12
3
4 128/25
13
14
20
*
30
40
51 52
NumberNode
IpTree
11. NumberNode의 역할
● 트리 구조 상의 한 노드를 표현
● 노드 데이터 보관
○ 노드의 값을 가짐
■ 정확한 값: 예 - 1, 10, 128
■ 패턴: 전체 또는 네트워크
● *
● 64/26
● 특정 숫자가 노드 데이터와 매칭되는 여부
● 노드 구성 관련
○ 자식 노드 생성 기능
○ 특정 숫자에 매칭 되는 자식 노드 찾아주는 기능
12. NumberNode: 객체 생성 부분 1
public class NumberNode {
private Map<String, NumberNode>
simpleChildNodeMap = // <값, 자식노드> 구성
new HashMap<String, NumberNode>();
private List<NumberNode> patternChildNodes = // 패턴 자식 목록
new ArrayList<NumberNode>();
private final String number; // 노드가 가진 값
private boolean isSimpleNumber; // 정확한 매칭 값인지 여부
private int filterNumber; // 네트워크 주소인 경우 사용
private int lastValueOfNetworkNumber; // 네트워크 주소인 경우 사용
private boolean allAccept; // 값이 "*" 인지 여부
private static int[] filterNumbers = { // 네트워크 주소 처리에 사용
0x00, // 24
0x80, // 25
0xC0, // 26
0xE0, // 27
0xF0, // 28
0xF8, // 29
0xFC // 30
};
자식 노드 구성 예:
simpleChildNodeMap = {
"1": childNodeX("1"),
"2": childNodeY("2"),
"5": childNodeZ("5")
}
patternChildNodes = [
childNodeP("*"),
childNodeQ("64/26")
]
자식 노드 보관 용도
23. IpFilter
● 주요 기능
○ IP가 차단 IP인지 확인
○ IP가 허용 IP인지 확인
○ 차단/허용 중 어떤 규칙을 먼저 적용할지
○ 두 규칙에 일치하지 않을 경우 허용할지 여부 지정
● 구성
○ Config: 설정 정보
○ IpFilter: 인터페이스
○ ConfigIpFilter: IpFilter의 구현
24. Config 클래스: 설정 정보 표현
public class Config {
private boolean defaultAllow;
private boolean allowFirst;
private List<String> allowList = new ArrayList<String>();
private List<String> denyList = new ArrayList<String>();
public void setDefaultAllow(boolean defaultAllow) {
this.defaultAllow = defaultAllow;
}
public boolean isDefaultAllow() { return defaultAllow; }
public void allow(String ip) {
allowList.add(ip);
}
public void deny(String ip) {
denyList.add(ip);
}
public void setAllowFirst(boolean allowFirst) {
this.allowFirst = allowFirst;
}
public boolean isAllowFirst() { return allowFirst; }
public List<String> getAllowList() { return allowList; }
public List<String> getDenyList() { return denyList; }
}
Config config = new Config();
config.setDefaultAllow(false);
config.setAllowFirst(flase);
config.allow("1.2.3.4");
config.allow("10.20.30.40");
config.deny("1.2.3.5");
config.deny("10.30.*");
config.deny("10.40.80.*");
25. ConfigIpFilter 클래스
public class ConfigIpFilter implements IpFilter {
private boolean defaultAllow;
private IpTree allowIpTree;
private IpTree denyIpTree;
private boolean allowFirst;
public ConfigIpFilter(Config config) {
defaultAllow = config.isDefaultAllow();
allowFirst = config.isAllowFirst();
allowIpTree = makeIpTree(config.getAllowList());
denyIpTree = makeIpTree(config.getDenyList());
}
private IpTree makeIpTree(List<String> ipList) {
IpTree ipTree = new IpTree();
for (String ip : ipList) ipTree.add(ip);
return ipTree;
}
@Override
public boolean accept(String ip) {
if (allowFirst) {
if (allowIpTree.containsIp(ip)) return true;
if (denyIpTree.containsIp(ip)) return false;
} else {
if (denyIpTree.containsIp(ip)) return false;
if (allowIpTree.containsIp(ip)) return true;
}
return defaultAllow;
}
}
Config config = new Config();
config.setDefaultAllow(false);
config.setAllowFirst(flase);
config.allow("1.2.3.4");
config.allow("10.20.30.40");
config.deny("1.2.3.4");
config.deny("10.30.*");
config.deny("10.40.80.*");
IpFilter filter = new ConfigIpFilter(config);
filter.accept("10.20.30.40"); // true: allow 규칙
filter.accept("10.30.50.51"); // false: deny 규칙
filter.accept("1.2.3.4"); // false: deny 규칙 먼저
filter.accept("101.1.2.3"); // false: defaultAllow
26. IpFilter의 성능 1
● 성능 검사에는 비교 대상 필요
○ 비교 대상 구현 (ListIpFilter)
■ 리스트 이용 IP 패턴 목록 유지
■ 순차적으로 IP 패턴 비교
● IP 패턴
○ IP 패턴 37,538개
■ https://github.com/madvirus/ip-filter/wiki/ip-list-config-for-performance-test
○ 패턴에 포함되는 IP 총 개수 약 3억 3천만
27. IpFilter의 성능 2
● 테스트 방법
○ 37,538개 IP 패턴을 차단 IP 목록으로 설정
○ 이 IP 패턴 목록 중 랜덤하게 5개 패턴 도출
○ 5개 패턴에 속한 전체 IP들을 검사
● 트리 기반 IpFilter와 ListIpFilter에 대해
○ 위 테스트 방법을 5회 진행해서 결과 값 구함
■ 실행 시간
■ 1개 당 검사 시간 = 실행 시간 / IP 개수
● 테스트 장비 (노트북)
○ Intel Core i5-2457M @1.6 GHz
○ Win 7 64b
○ JDK 6 (1.6.0_26)
28. IpFilter의 성능 3
● 멀티 쓰레드 상황
○ 쓰레드 10개, 20개, 50개 실행
○ 각 쓰레드 마다 '위 테스트 방법'으로 실행
○ 단, 각 쓰레드는 5개 패턴의 전체 IP 개수가 아닌
최대 10만개만 검사
■ 각 쓰레드가 최대한 겹처서 실행되도록 하기 위함
29. IpFilter 성능 결과1 - 1개 쓰레드
트리 방식 리스트 방식
회차 실행 회수 실행 시간
(밀리초)
평균
(밀리초)
실행 회수 실행 시간
(밀리초)
평균
(밀리초)
1 199,680 678 0.003400 50,944 28,631 0.562029
2 1,450,240 3,648 0.002516 1,212,928 181,893 0.152436
3 22,016 196 0.008931 12,800 6,397 0.499768
4 804,352 2,109 0.002622 377,088 152,709 0.404970
5 1,120,256 2,723 0.002431 273,920 14,964 0.054632
평균 0.003980 평균 0.334767
30. IpFilter 성능 결과2 - 10개 쓰레드
트리 방식 리스트 방식
회차 실행 회수 실행 시간
(밀리초)
평균
(밀리초)
실행 회수 실행 시간
(밀리초)
평균
(밀리초)
1 695,840 7,592 0.010912 672,033 745,404 1.110667
2 816,640 6,325 0.007746 847,712 837,813 0.988323
3 698,304 6,301 0.009024 720,576 633,170 1.101748
4 901,120 8,216 0.009118 792,000 976,851 1.233398
5 664,096 5,576 0.008397 693,024 1,162,127 1.677894
평균 0.009039 평균 1.205352
31. IpFilter 성능 결과3 - 20개 쓰레드
트리 방식 리스트 방식
회차 실행 회수 실행 시간
(밀리초)
평균
(밀리초)
실행 회수 실행 시간
(밀리초)
평균
(밀리초)
1 1,215,400 21,850 0.017978 1,693,024 3,436,681 2.029907
2 1,549,088 22,001 0.014203 1,248,832 1,447,470 1.159059
3 1,676,480 25,228 0.015048 1,630,304 5,131,852 3.147789
4 1,383,552 17,147 0.012394 1,633,728 4,168,919 2.551783
5 1,598,049 20,416 0.012776 1,516,736 2,786,563 1.837211
평균 0.014480 평균 2.145150
32. IpFilter 성능 결과4 - 평균/편차
트리 방식 리스트 방식
쓰레드 평균 편차 평균 편차
1 0.003980 0.002794 0.334767 0.221089
10 0.009039 0.001183 1.205352 0.280404
20 0.014480 0.002230 2.145150 0.750186
50 0.033292 0.007773 X X
39. 문자열로 설정하고 싶어요
# 주석도 넣고,
order allow,deny
default true
allow from 1.2.3.4
allow from 1.2.3.* # 뒤에 주석
...
...
deny from all
● 장점
○ DSL로서 이해가 쉬움(Domain Specific
Language)
○ 파일 등으로 설정시 작성/편집이 용이
○ HTTP 등으로 설정 정보 제공시 응답 데이터 생
성 용이
● 필요한 것
○ 문자열로부터 Config 객체 생성하기
○ 문법을 만들고, 파서로 좀 해보고 싶은데...
40. 문맥 자유 문법과 파서
-- 문법은 대충 이런식 (컴파일러 시간에 배웠던 기억은 있으나, 내용 자체는 거의 기억 안 남)
conf : confPart? (eol confPart)*
confPart: commentPart | orderPart | defaultPart | allowOrDenyPart | emptyLine
commentPart: '#' ANY
orderPart: "order" orderValue commentPart?
orderValue: "allow" "," "deny" | "deny" "," "allow"
defaultPart: "default" BOOLEAN commentPart?
allowOrDenyPart: allow | deny
allow: "allow" "from" ipPattern commentPart?
deny: "deny" "from" ipPattern commentPart?
ipPattern: "all" | (d+.){1,3}(*) | (d+.d+.d+.d+/d+) | (d+.d+.d+.d+)
● 자바용 파서 생성기 : ANTLR 등 존재
○ 사용법이 다소 복잡 (문법 파일 만들고, 코드 생성하고 등등)
● 사용법이 쉬우면서 자바와 연동되는 파서 필요
○ 마침 공부중이던 Scala의 Combinator Parser 선택
41. Scala Combinator Parser
● Scala가 기본으로 제공하는 파서
● 기본적인 문맥 자유 문법 지원
○ 코드에서 문법과 결과를 바로 표현
● 자바 코드에서 손쉽게 호출 가능
43. 쉬운 사용 위한 보조 클래스
// Java 코드
public class FileConfigFactory
extends ConfigFactory {
@Override
public Config create(String value) {
return new ConfParser()
.parse(readFromFile(value));
}
private String readFromFile(String fileName) {
try {
return IOUtil.read(
new FileReader(fileName));
} catch (IOException e) {
throw new ConfigFactoryException(e);
}
}
// Scala 코드
class ConfParser extends Conf {
def parse(confText: String): Config = {
val result = parseAll(conf, confText)
if (result.successful)
result.get
else
throw new ConfParserException(result.toString)
}
}
45. 내용 정리
● 트리 기반의 IP 패턴 목록 관리
○ 패턴 개수에 상관없이 일정한 탐색 속도 제공
○ 다중 쓰레드 접근 시에도 성능 저하 상대적 낮음
● SLF4J 방식 컴포넌트 구성
○ 동적 클래스 로딩이 아닌 jar 교체 방식
● Scala를 이용한 문법/파서 구현
○ 외부 DSL 구현을 쉽게 할 수 있도록 도와줌
○ 자바와의 연계가 쉬움
46. 관련 자료
● ip-filter 소스
○ 소스: https://github.com/madvirus/ip-filter
○ 사용법: https://github.com/madvirus/ip-
filter/wiki/HOME_kr
● SLF4J 소스
○ https://github.com/qos-ch/slf4j
● Scala
○ http://www.scala-lang.org/
○ 쉽게 배워서 빨리 써먹는 스칼라 프로그래밍 (번역)
■ http://kangcom.com/sub/view.asp?
sku=201304120013
47. 광고
내가 만든 코드를 함께 리뷰할 선배 프로그래머가 없나요?
주변 프로그래머들이 너무 바빠서 코드 리뷰할 시간이 없나요?
이런 상황이라면, 고민하지 마시고 연락주세요.
함께 코드를 보고 논의하고 수정하는 시간을 가져보아요~
1. 시간/장소: 저녁 시간대, 당산~사당 사이의 커피집
2. 준비물: 함께 코드를 볼 수 있는 노트북 및 코드 수정이 가능한 개발도
구(이클립스 등)
3. 코드 리뷰 가능한 범위: 자바 기반의 코드
4. 연락 방법
a. 카페 댓글(http://cafe.daum.net/javacan/MsBU/13 글에 댓글)
b. 트위터 멘션 또는 DM (@madvirus)
c. 이메일 (madvirus@madvirus.net)
d. 페이스북 (https://www.facebook.com/beomkyun.choi)
5. 개발 얘기도 합니다.