iptable과 ipset을 연동하여 대량 IP 효과적으로 차단하기

Referer 주소가 없는 방문자의 정체

언제부터 인지는 알 수 없지만 필자가 운영하고 있는 워드프레스 블로그의 방문자 로그에 이상한 로그가 잡히기 시작했었다. 바로 다음과 같은 로그다.

구글이나 네이버와 같은 검색엔진을 통해 유입이 되는 경우 95% 이상의 비율로 Referer에 Google이나 Naver, Bing 등 검색엔진의 URL 주소가 기록된다. 하지만 앞 화면에 보이는 것과 같은 접속 이력은 Referer 주소가 없다.

이 방문자 접속 이력은 필자의 워드프레스 블로그에 직접 만들어 설치한 방문자 카운터 플러그인에의해 기록되는 방문자 기록이다. 방문자 카운터의 플러그인의 구조상 브라우저를 통해 방문한 다음 브라우저로 내려간 JavaScript가 실행되어 방문자 카운터 플러그인의 PHP 코드를 호출해 방문자의 접속 이력이 기록되기 때문에 크롤러나 봇이 취약점이 있는지 “찔러보는” 경우 방문자 카운터가 동작하지 않는 경우가 대부분이다. 또한 스팸 댓글 봇처럼 텍스트만 긁어가고 JavaScript는 실행하지 않는 그런 봇의 경우에도 방문자 접속이력에 기록되지 않는다.

그래서 확인한 결과 AI 학습 또는 정밀한 검색 인덱싱 목적으로 사람이 브라우저를 통해 접근하는 것과 동일하게 데이터를 긁어가기 위해 JavaScript까지도 실행하는 Headless Chrome 또는 Selenium과 같은 최신 웹브라우저의 엔진을 통째로 사용하여 크롤링 하는 경우 방문자 카운터가 동작하기도 하는 것으로 보인다.

다만 OpenAI, Gemini, ChatGPT 처럼 양심적으로 크롤러의 소속과 목적을 밝히고 크롤링하는 것이 아니라 무언가 불순한 목적이기 때문에 소속과 목적을 밝히지 않고 크롤링해가는 것이다.

Referer 주소가 없는 크롤러 또는 봇의 발신지 IP 확인

그래서 IP를 조회해본다.

IP 주소가 일반 ISP 통신사에 할당된 주소가 아니라 현재 중국의 텐센트에서 운영하고 있는 데이터센터에 할당된 주소다. 즉 이 IP는 데이터센터의 서버에 할당된 IP일 가능성이 99%이기 때문에 사람이 브라우저를 사용해 직접 접속한 것이 아니라는 의미다. 그래서 차단하면 된다.

크롤러와 봇의 효과적인 접근 차단 방법

처음엔 이 트래픽을 차단하기 위해 .htaccess에서 Referer 주소가 없고 접근하는 경로에 “/tag/” 가 포함된 경우 차단하도록 설정하도록 했다. 하지만 .htaccess 파일은 접근이 있을 때 마다 파일을 열고 정책을 읽어 차단하기 때문에 부하 측면에서 문제가 되고 아파치 웹서버 실행 시 한번만 여는 httpd.conf로 옮기게 되면 설정 파일이 과도하게 복잡해져서 맘이 편치 않았다.

그래서 클라우드플레어도 사용해봤지만 무료 플랜에서는 트래픽이 일본 등 해외를 거치게 되기 때문에 속도가 느려고 룰의 개수도 5개로 제약이 있어 사용하지 않기로 했다.

결국 최종적으로 사용하게 된 솔루션이 바로 IPTable이다. 하지만 문제는 이런 유사한 트래픽이 수많은 IP 대역, 심지어 홍콩 뿐만 아니라 베이징, 싱가폴, 미국 산타클라라, 멕시코의 멕시코시티 등 텐센트와 화웨이의 데이터센터에서 불규칙하게 유입된다는 의미다. /24 (C 클래스)로 차단하더라도 1천개 이상의 IP 주소가 될 가능성이 높다. 때문에 단순히 IPTABLE에 한줄 한줄 추가해 차단하게 되면 아무래도 관리와 성능 측면에서 매우 곤란해진다.

IPTable은 1000개의 IP 주소가 정책에 포함되어 있다면 무조건 위에서부터 아래로 순차적으로 “검색”해 해당되는 정책이 있다면 차단하는 방식이기 때문이다.

IPSet의 동작 원리

그래서 IPTable을 사용하면서 효과적으로 대량의 IP를 차단하기 위해 선택한 솔루션이 바로 IPTable과 연동 가능한 IPSet 이다. ipset은 차단 또는 허용하고자 하는 IP 주소 또는 IP 대역(/24 등과 같은 CIDR)을 등록하면 해당 IP 주소 또는 IP의 네트워크 주소를 Hash함수에 넣어 MD(메시지 다이제스트, 흔히 Hash값이라고 함)를 추출해 메모리에 저장한다. 그리고 특정 IP에서 접근을 시도하는 패킷이 서버의 네트워크 커널에 도착했을 때 해당 IP 주소를 가로챈 다음 IP 주소에서 MD를 추출해 메모리에 저장하고 있는 MD와 일치하는 값이 있는지 확인하여 차단 여부를 결정한다.

다만 이 때 저장하고 있는 MD를 찾아갈 때 IPTable의 정책과 같이 하나씩 검색해 가는 것이 아니라 Hash 테이블을 활용해 순차 검색에 비해 엄청나게 빠르게 찾아간다는 점이다. 그래서 IPSet을 사용하면 차단 대상 IP가 많아도 IPTable만 사용했을 때 보다 엄청나게 빠르게 동작하게 된다.

IP를 기반으로 위치 정보 확인하는 파이썬 코드 작성하기

IP 주소를 확인 후 앞의 화면에서 처럼 하나씩 하나씩 위치를 조회해볼 수는 없다. 그래서 Python으로 코드를 작성해 대량의 IP 주소를 한꺼번에 조회한 다음 ipset 명령으로 차단할 수 있는 명령어를 조합해 출력하도록 했다.

# ip_geolocate_and_annotate.py
# 사용법: Python 3.8+ 환경에서 실행
# 필요 패키지: requests  -> 설치: pip install requests
# 수정된 내용: IP를 /24 서브넷으로 변환하고, 국가에 따라 ipset 명령어를 생성합니다.

import requests

ips = [
# 위치를 확인할 IP 주소 목록
"42.230.221.100", "43.129.177.16","159.138.153.162"
]

# 유럽 연합(EU) 회원국 국가 코드 목록, eu_black_list 그룹 (net/hash)에 추가하기 위해 확인
EU_COUNTRY_CODES = [
    "AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR",
    "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL",
    "PT", "RO", "SE", "SI", "SK"
]

# ip-api.com 에서 제공하는 일괄 처리(Batch) API 예시 (최대 100개 IP 동시 조회 가능)
API_URL = "http://ip-api.com/batch?fields=query,status,countryCode,org,isp,as,asname,city,message"

def main():
    generated_commands = []
    # 한 번에 최대 100개의 IP를 리스트로 묶어서 처리
    for i in range(0, len(ips), 100):
        batch_ips = ips[i:i+100]
        try:
            # POST 방식으로 IP 리스트를 전송
            r = requests.post(API_URL, json=batch_ips, timeout=10)
            r.raise_for_status()
            data = r.json()

            for item in data:
                ip = item.get("query", "-")

                if item.get("status") == "success":
                    ip_subnet = ".".join(ip.split('.')[:-1]) + ".0/24"
                    country_code = item.get("countryCode")
                    # org 값이 없으면 isp, asname, as 순으로 대체해서 회사명을 가져옵니다.
                    org = item.get("org") or item.get("isp") or item.get("asname") or item.get("as") or "-"
                    city = item.get("city") or "-"
                    # 국가에 따라 EU, US, KR, ETC로 분류함
                    if country_code in EU_COUNTRY_CODES:
                        blacklist_group = "eu_black_list"
                    elif country_code == "US":
                        blacklist_group = "us_black_list"
                    elif country_code == "KR":
                        blacklist_group = "korea_black_list"
                    elif country_code == "CN":
                        blacklist_group = "china_black_list"
                    else:
                        blacklist_group = "etc_black_list"

                    comment = f'{org.strip()} {city.strip()}'.strip()
                    command = f'sudo ipset add {blacklist_group} {ip_subnet} comment "{comment}"'
                    generated_commands.append({'command': command, 'org': org.strip()})
                else:
                    print(f"# IP {ip} 정보 조회 실패: {item.get('message', '-')}")

        except Exception as e:
            print(f"조회 중 오류 발생: {e}")

    # 회사명(org)을 기준으로 오름차순 정렬
    generated_commands.sort(key=lambda x: x['org'])

    # 정렬된 명령어 출력
    for item in generated_commands:
        print(item['command'])

if __name__ == "__main__":
    main()

이 코드를 .py 파일로 저장한 다음 python 명령으로 실행하면 다음과 같은 결과가 출력된다.

(.venv) [taeho@app geoip]$ python geoip.py
sudo ipset add china_black_list 42.230.221.0/24 comment "China Unicom Henan Province Network Zhengzhou"
sudo ipset add etc_black_list 159.138.153.0/24 comment "Huawei International Pte. Ltd. Hong Kong"
sudo ipset add etc_black_list 43.129.177.0/24 comment "Tencent Cloud Computing Hong Kong"
(.venv) [taeho@app geoip]$ 

42.230.221.0/24의 경우 차이나 유니콤이라는 통신사의 IP이고 중국 허난성 정저우라는 도시에서 사용되고 있다. 데이터센터 여부는 알 수 없지만 일반 사용자가 사용하는 IP는 아니므로 차단하기로 했다.

또한 159.138.153.0/24는 화웨이의 홍콩IP다. 이 IP는 GEOLocation에서 조회하니 데이터센터로 출력된다.

마지막 43.129.177.0/24는 텐센트 클라우드 홍콩 리전에 할당된 IP다. 99% 데이터센트의 서버 IP 주소다.

IPSET의 Hash/Net 그룹 생성하고 IPTable 정책 연동하기

이제 이 명령어를 리눅스 운영체제에서 실행해주면 china_black_list, etc_black_list 라는 hash/net 그룹에 추가되고 이 그룹들을 IPTable에 차단 정책에 객체로 등록해주면 실제 차단이 일어난다. 다음의 명령어를 통해 ipset에 생성한 hash/net 그룹을 IPTable 정책에 연동할 수 있다.

sudo iptables -I INPUT  행위치(번호)  -m set --match-set  객체명  src -m comment --comment "Block korea isp and idc via ipset(설명)" -j DROP

다음과 같이 IPTable에 등록된 IPSet의 Hash/Net 그룹 들을 확인할 수 있다. IPTable 정책의 행번호를 확인하는 방법은 아래의 명령어 목록에 있다.

taeho@www:~$ 
taeho@www:~$ sudo iptables -L INPUT
Chain INPUT (policy ACCEPT)
target     prot opt source      destination         
ACCEPT     all  --  anywhere    anywhere   state RELATED,ESTABLISHED
ACCEPT     icmp --  anywhere    anywhere      
ACCEPT     all  --  anywhere    anywhere    
ACCEPT     udp  --  anywhere    anywhere   udp spt:ntp
DROP       all  --  anywhere    anywhere   match-set china_black_list src /* Block China IDC via ipset */
DROP       all  --  anywhere    anywhere   match-set etc_black_list src /* Block etc isp and idc via ipset */
DROP       all  --  anywhere    anywhere   match-set korea_black_list src /* Block korea isp and idc via ipset */
DROP       all  --  anywhere    anywhere   match-set eu_black_list src /* Block EU isp and idc via ipset */
DROP       all  --  anywhere    anywhere   match-set us_black_list src /* Block US isp and idc via ipset */
ACCEPT     tcp  --  anywhere    anywhere   state NEW tcp dpt:http
ACCEPT     tcp  --  anywhere    anywhere   state NEW tcp dpt:https
ACCEPT     tcp  --  anywhere    anywhere   state NEW tcp dpt:9999
ACCEPT     tcp  --  anywhere    anywhere   state NEW tcp dpt:8888
DROP       all  --  anywhere    anywhere            
taeho@www:~$ 

match-set china_black_list src 라는 정책이 바로 ipset에서 정의한 china_black_list 라는 Hash/Net 그룹을 Source IP (src)로 적용하라는 의미다.

특정 패킷이 네트워크 커널에 도착하면 IP 주소를 기반으로 IPTable의 맨 위 정책부터 검사하다가 첫 번째 DROP문에서 ipset의 china_black_list 라는 그룹에 포함되어 있는지 Hash Table을 활용해 찾아가고 없다면 두번째 match-set 정책을, 또 없다면 세번째와 네번째, 다섯번째 정책을 차례대로 비교해 차단 여부를결정한다.

이렇게 열흘정도 부지런히 사람이 아닌 것으로 추정되는 허위 방문자의 IP를 수집해 차단하다 보니 방문자 수가 꽤 줄어들었다. 티스토리나 워드프레스의 방문자 카운터가 제공하는 일 방문자 수와 구글 애드센스가 보여주는 방문자 수를 비교하면 구글 애드센스의 방문자 수가 훨씬 적었는데 그 이유를 알 것 같았다.

IPSet 관련 명령어 모음

ipset을 설치하고 hash/net 그룹을 생성하고 생성한 hash/net 그룹에 IP를 추가 또는 삭제하는 명령과 추가적인 관리 목적의 명령을 소개하며 포스트를 마무리 한다. IPTable의 일부 명령어도 함께 소개한다.

o ipset 명령어 설치 방법 (Ubuntu 24.04 기준)
 $ sudo apt update && sudo apt install ipset -y

o 네트워크 리스트 그룹 객체 생성
 $ sudo ipset create 객체명 hash:net 

o 객체 보기, 객체명 없으면 전체 보여줌
 $ sudo ipset list 객체명   

o IP 추가 [즉시 적용됨]
 $ sudo ipset add 객체명  ip주소

o IP 삭제  [즉시 적용됨] 
 $ sudo ipset del 객체명  IP주소 

o IPSet 객체(정책) 정보 영구저장
 $ sudo ipset save | sudo tee /etc/iptables/ipsets > /dev/null 

o 객체명 사용해서 IPTable에 차단 정책 적용하기
 $ sudo iptables -I INPUT  행위치(번호)  -m set --match-set  객체명  src -m comment --comment "Block korea isp and idc via ipset(설명)" -j DROP

o iptable INPUT 체인의 정책과 행번호 함께 표시하기
 $ sudo iptables -L INPUT --line-numbers 

o IPTable 정책 삭제하기
 $ sudo iptables -D INPUT [행번호] 

o iptable 정책 저장하기
 $ sudo netfilter-persistent save 

o 모니터링하기 5초 간격으로 변경된 항목을 하일라이트 표시
 $ watch -d -n 5 "sudo iptables -L INPUT -v -n

ipset과 iptable을 연동하여 지속적으로 차단하다 보니 이제 좀 허수가 정리가 되어보인다.

#iptable #ipset

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다