Devin.KR

정책 규칙 순서

100분 안팎

학습 목표

패킷 정보와 순서 있는 규칙을 입력받아 첫 일치 결과와 기본 차단을 출력합니다.

개념

왜 순서를 먼저 계산하는가

게스트 학교 웹을 차단하는 규칙을 추가했는데 계속 접속되는 상황을 생각합니다. 앞에 모든 역할의 웹을 허용하는 넓은 규칙이 있으면 뒤의 제한에 도달하지 않을 수 있습니다. 이번에는 실제 장비를 건드리지 않고 패킷 한 건과 규칙 배열을 입력해 첫 일치 결과를 계산합니다. 규칙 표를 검토할 때 “여기에 drop이 있습니다”라고 설명하는 데서 멈추지 않고 어느 규칙이 먼저 맞는지 증거로 보여 주는 연습입니다.

모델의 판정 범위를 정합니다

이 평가기는 하나의 순서 있는 목록에서 accept 또는 drop 중 처음 일치하는 종료 동작을 반환합니다. nftables 전체를 복제하지 않습니다. jump·return·여러 base chain·연결 상태·인터페이스·NAT은 계산하지 않습니다. 실제 nftables에서는 한 base chain의 accept 뒤에도 다른 hook이나 base chain에서 drop될 수 있습니다. 그러므로 모델에서 accept를 얻었다고 실제 연결 성공이라고 보고하지 않습니다. 이번 목표는 규칙의 겹침과 기본 차단을 코드로 설명하는 것입니다.

입력 계약을 읽습니다

표준 입력 전체는 JSON 객체 하나입니다. packet에는 src와 dst IPv4 주소 문자열, proto의 tcp 또는 udp, dport의 1부터 65535까지 정수가 있습니다. rules는 배열이며 각 행은 src와 dst CIDR, proto, dport, action을 가집니다. 규칙 proto의 *는 두 프로토콜 모두, 규칙 dport의 0은 모든 목적지 포트를 뜻하는 교육용 표기입니다. 실제 서비스 포트 0을 시험하는 것이 아니며 패킷의 dport에 0을 넣지 않습니다.

출력 계약을 고정합니다

일치하면 대문자 ACCEPT 또는 DROP 뒤에 규칙의 1부터 시작하는 번호를 출력합니다. 어떤 규칙도 일치하지 않으면 DROP DEFAULT를 출력합니다. 빈 규칙 배열도 같은 기본 차단입니다. stdout에 디버그용 배열이나 한국어 설명을 덧붙이면 채점 출력과 달라집니다. 번호는 규칙에 저장된 id가 아니라 입력 배열 위치입니다. 예외처리는 유효 JSON·IPv4·정해진 필드가 입력된다는 범위 밖이므로 모든 입력 오류를 정책 차단으로 바꾸지 않습니다.

주소는 문자열 접두어가 아닙니다

10.20.4로 시작하는 문자열 비교는 10.20.40과 다른 망을 혼동합니다. ipaddress의 ip_address로 패킷 주소를 만들고 ip_network로 CIDR을 만든 뒤 in으로 포함을 검사합니다. 규칙 CIDR은 네트워크 경계에 맞는 유효 값만 입력됩니다. 10.20.40.0/24의 마지막 값인 .255도 계산상 범위 안이지만 그 주소로 실제 호스트 연결을 하겠다는 의미는 아닙니다. 주소 포함 계산과 유효한 단말 주소 배치는 구별합니다.

한 행의 조건은 모두 맞아야 합니다

출발지 포함, 목적지 포함, 프로토콜 일치 또는 *, 목적지 포트 일치 또는 0을 and로 묶습니다. 목적지와 포트만 맞고 출발지가 다르면 다른 역할의 요청입니다. proto == tcp or udp처럼 쓰면 뒤 문자열 자체가 참으로 읽힐 수 있습니다. 후보 비교는 proto in 목록 또는 명시적인 두 비교를 사용합니다. 한 조건을 잘못 or로 연결하면 관리 서버의 포트만 맞는 요청이 역할 검사 없이 허용되는 오류를 만들 수 있습니다.

첫 결과를 찾으면 멈춥니다

evaluate 함수에서 enumerate(rules, 1)로 순번과 규칙을 함께 순회합니다. 네 조건이 참이면 action.upper()와 번호를 즉시 반환합니다. 모든 행을 끝낸 뒤에만 기본 차단을 반환합니다. 결과 변수를 매번 덮어쓰고 끝에서 출력하면 마지막 일치 규칙이 이겨 요구와 달라집니다. continue는 해당 행을 건너뛰고 다음 규칙을 보므로 판정을 확정하는 도구가 아닙니다. 첫 일치 뒤 반환은 순서를 정책 의미에 포함시킵니다.

넓은 규칙이 예외를 가립니다

예를 들어 출발지 0.0.0.0/0, 목적지 학교 서버 /32, TCP 8080 accept를 앞에 두고 게스트 /24 drop을 뒤에 두면 게스트 웹은 처음 accept에서 끝납니다. 해결은 막연히 “차단이 우선”이라고 외우는 것이 아닙니다. 구체적 금지 예외를 앞에 두거나 넓은 허용의 출발지 자체를 학생·교직원으로 제한합니다. 요구가 역할 허용 목록이라면 후자가 검토하기 쉬우며 시험 입력은 두 설계가 같은 요구를 만족하는지 확인하게 합니다.

정확한 역할 허용을 먼저 검토합니다

교직원 관리 2222 허용이 있다고 같은 서버의 모든 포트를 허용하지 않습니다. 입력의 목적지 CIDR이 /32인지, proto가 tcp인지, dport가 2222인지 확인합니다. 규칙 dport 0과 proto *를 같이 쓰면 해당 주소 범위의 모든 TCP·UDP 목적지 포트가 포함됩니다. 이것이 업무 요구인지 별도로 묻습니다. 편리한 와일드카드가 실제 요청보다 넓게 열리지 않는지 확인하는 것이 규칙 평가기를 만드는 실무 목적입니다.

경계 사례로 알고리즘을 흔듭니다

규칙 없음, 첫 규칙 일치, 두 번째 규칙 일치, 주소가 /24 바로 밖인 입력을 각각 넣습니다. 같은 IP와 같은 포트라도 UDP로 바꾸면 TCP 허용이 적용되지 않아야 합니다. /32 목적지를 다른 서버 주소로 바꿔 기본 차단도 확인합니다. 규칙 포트 와일드카드와 프로토콜 와일드카드는 별도로 시험합니다. 한 개의 성공 입력만 반복하면 조건 중 하나를 빼먹은 구현도 통과하므로 서로 다른 실패 이유를 의도적으로 만듭니다.

오류 메시지를 분류합니다

JSONDecodeError는 따옴표·쉼표 등 입력 형식을 확인하라는 뜻이며 방화벽이 요청을 차단했다는 결과가 아닙니다. KeyError는 정해진 필드 이름과 packet·rules 구조를 대조합니다. ValueError가 CIDR을 가리키면 /24의 네트워크 주소와 IPv4 범위를 확인합니다. NameError는 가져오지 않은 ip_address 같은 이름을 확인합니다. 오류를 잡아 DROP DEFAULT로 출력하면 프로그램 고장을 정상 정책 판정처럼 숨기므로 학습 단계에서는 원인을 수정합니다.

리팩터링 뒤에도 의미를 보존합니다

조건식을 matches 함수로 분리해도 반환 위치를 바꾸지 않습니다. 테스트가 규칙 순서를 표현하도록 앞 accept·뒤 drop 입력과 그 반대 입력을 같이 둡니다. 두 결과가 달라지는 것은 이 목록 모델에서 정상입니다. 리팩터링 전후 같은 JSON에 같은 판정을 얻는지 확인합니다. 규칙을 CIDR 길이로 정렬하는 최적화는 이 과제에서 금지한 의미 변경입니다. 라우팅의 최장 접두어 선택을 방화벽 규칙 목록에 그대로 적용하지 않습니다.

학교망 표와 연결합니다

정책 CSV의 한 행을 JSON 규칙으로 바꿀 때 신규 요청의 출발지·목적지·포트를 씁니다. 브라우저 과제에서는 hook과 상태가 없으므로 input DNS와 forward 웹은 각각 별도 분석 대상으로 생각합니다. SNAT 뒤의 198.51.100.1을 학생 source로 넣으면 역할 규칙에 안 맞습니다. 어떤 단계의 주소를 모델에 넣었는지 주석으로 적습니다. 실제 학교 정책은 다음 레슨의 상태 응답과 체인 위치까지 갖추고 Linux에서 검증합니다.

완성 기준

starter의 evaluate는 항상 기본 차단을 반환합니다. 조건 네 개와 첫 일치 즉시 반환을 구현해 제공 경계 입력 전체를 통과시킵니다. solution은 모범 알고리즘이고 정책 자체의 업무 승인을 대신하지 않습니다. 본문 코드는 입력을 고정한 실행 예이고 실습 코드는 sys.stdin을 읽습니다. 표준 입력 계약을 유지하면서 새 테스트를 추가하여 “목적지만 맞을 때는 허용하지 않습니다” 같은 자신의 검토 문장을 재현 가능한 결과로 바꿉니다.

실제 nftables의 여러 체인과 verdict 범위는 공식 매뉴얼에서 확인합니다. 실습 평가기는 해당 장비의 전체 판정기를 재현하지 않습니다.

따라하기

첫 일치의 효과 확인

Python 3에서 다음 계산을 실행하고 결과를 정책 판단에 연결합니다.

rules = [('accept', True), ('drop', True)]
for number, (action, matches) in enumerate(rules, 1):
    if matches:
        print(action.upper(), number)
        break

실행 결과

ACCEPT 1

전송 프로토콜 대조

Python 3에서 다음 계산을 실행하고 결과를 정책 판단에 연결합니다.

packet = {'proto': 'udp', 'dport': 8080}
print('protocol matches', packet['proto'] == 'tcp')
print('port matches', packet['dport'] == 8080)

실행 결과

protocol matches False
port matches True

JSON 입력으로 완성

아래 브라우저 실습에서 evaluate를 구현합니다. 넓은 허용과 구체적 차단을 뒤집은 두 입력, 빈 배열, 주소 경계, 와일드카드를 모두 검사합니다. stdout에는 판정 한 줄만 출력합니다.

확인 문제

실습

표준 입력 JSON의 packet(src·dst IPv4, proto tcp/udp, dport 정수 1~65535)과 rules 배열을 읽습니다. 규칙은 src·dst 유효 IPv4 CIDR, proto tcp/udp/*, dport 1~65535 또는 와일드카드 0, action accept/drop입니다. 네 조건 모두 일치하는 첫 행에서 ACCEPT 또는 DROP과 1부터 시작하는 행 번호를 출력합니다. 없으면 DROP DEFAULT입니다. 상태·NAT·여러 체인은 모델 범위 밖입니다. 앞뒤 규칙 겹침·빈 목록·CIDR 경계·다른 프로토콜·포트 와일드카드를 통과시킵니다.

모범 답안
import sys,json
from ipaddress import ip_address,ip_network

def evaluate(packet,rules):
 src=ip_address(packet['src']);dst=ip_address(packet['dst'])
 for index,r in enumerate(rules,1):
  if (src in ip_network(r['src']) and dst in ip_network(r['dst'])
      and (r['proto']=='*' or r['proto']==packet['proto'])
      and (r['dport']==0 or r['dport']==packet['dport'])):
   return r['action'].upper()+' '+str(index)
 return 'DROP DEFAULT'

x=json.load(sys.stdin)
print(evaluate(x['packet'],x['rules']))

더 읽기

면접 질문

  • 방화벽 정책을 검증하는 실습 방법을 설명합니다.