본문 바로가기
ОКБ (실험 설계국)/Trouble Shooting

17. [Python] firewall-cmd rich rule 제어 오류

by Rosmary 2023. 9. 6.
728x90
반응형

 

 

 

* Error: Python subprocess로 firewall-cmd rich rule 제어 시 오류 발생(Error: INVALID_RULE: internal error in _lexer():)

* Environment: CentOS8, Python 3.11

 

 

최근 말단의 Linux 방화벽에 차단 정책을 자동으로 생성하는 시스템을 개인 프로젝트로 진행하고 있다.사실, 이전에 Bash로만 작성한 스크립트가 있는데, 외근 갔다가 올라오는 기차 안에서 심심한 김에 만든 것인데다 실시간 적용 기능도 없고, 무엇보다도 현재는 거의 사양된 iptables에 적용된 스크립트였기 때문에 언젠가 한 번 개선을 해야겠다고 마음먹고있던 참이었다.

 

최근에는 필자도 회사 일에만 파묻혀있기 싫어 일부러 개인 프로젝트에 비중을 옮겨가고 있는 상태다(일을 쉬엄쉬엄하라 했더니 업무 비스무리한 걸로 시간을 보낸다. 오랑캐는 오랑캐로 잡는 것(以夷伐夷)인가...). 그러다가 이번 주에 firewall-cmd 명령어로 rich-rule 제어를 위해 python 코드를 작성하던 중, 위와 같은 에러를 만났다. 

 

 

1. 문제의 발견

 

보통 OS 명령어를 python으로 실행하기 위해 subprocess라는 패키지에 존재하는 Popen()이나 run() 함수를 많이 사용한다. 그리고 OS 명령어는 보통 옵션 단위로 나뉘어져 List 형태로 들어간다. firewalld의 default zone 확인을 예시로 들자면,

 

*  명령어: firewall-cmd --list-all --zone=public

*  list 변환 형태:  ['firewall-cmd', '--list-all', '--zone=public']

 

처럼 변환이 되며, 실제 코드에는 아래와 같이 적용된다.

 

 

당연히 이 코드는 문제 없이 실행된다. 

 

 

대부분의 명령어는 python에서 위와 같은 형태로 에러 없이 잘 실행되는데, 필자가 진행하려고 하는 firewall-cmd의 rich-rule 관련 작업은 이러한 방식이 에러만을 뿜어내는 것을 확인했다.

 

*  명령어: firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="1.2.3.4" port port=514 protocol="udp" drop'

*  list 변환 형태: ['firewall-cmd', '--zone=public', '--add-rich-rule=\'rule family="ipv4" source address="1.2.3.4" port port=514 protocol="udp" drop\'']

 

 

 

 

분명, 첫 예시와 동일한 형태임에도 불구하고, 적용 결과에 대한 반환 없이 INVALID RULE이라는 에러만 뿜어낸다. 그럼  필자가 List 형태로 작성한 명령어에 문법적인 문제가 있는 것일까? 명령어 List를 아래와 같이 String 형태로 다시 변환하여 화면에 출력하도록 만들었다.

 

 

명령어를 복사해서 바로 실행해보면, Python과 달리, 정상적으로 실행됨을 확인할 수 있다.

 

실제로 public zone에 rich rule이 적용된 것도 확인이 된다.

 

 

 

2. 문제의 해결

 

이 이상한 현상을 해결하기 위해 요즘 핫한 Chat GPT에게 문의를 해보았다. 필자의 경우 정확성을 위해 부족하더라도 되도록 영어로 질문을 하면서 순도높은 결과물을 받아왔는데, 오늘따라 날이 더워서 ChatGPT가 더위를 먹은 것인지, 아니면 필자가 더위를 먹어 영어가 이상한 건지 GPT의 답변이 영 주제와는 딴판이다.

 

몇 번을 물어봐도 이상한 답변만 하는 GPT에 말 못알아듣는다고 조금 한탄했다.

 

답답하면 내가 뛰어야지. 직접 검색을 진행해보았다. 특이하게도 rich-rule과 관련된 작업을 거의 하지 않는지, 필자가 원하는 질문과 답변에 매우 유사한 내용 하나가 Stack Overflow 사이트에서 발견되었다.

 

심지어 질문자는 필자와 마찬가지로, "이거 직접 실행은 잘 되는데 왜 python에서만 안됨?" 이라는 질문을 던져놓아 필자가 관심을 가지고 답변을 보았다. 그런데 매우 단순하게도 list 형태로 명령어를 날리지 말고, String 형태로 Shell에 직접 실행할 수 있도록 코드를 작성하라는 하나의 답변만 존재하고 있었다. 

 

 

답변을 달아놓은 친구의 조언대로 필자도 코드를 바꿔보았다. 방화벽 정책을 원래 상태로 되돌린 뒤, 변경한 코드를 실행해보았다. 솔직히 단순하게 Shell에 직접 실행하도록 옵션하나 주고 string 형태로 명령어를 변경한 것 뿐이라 성공할 것이라는 기대는 1도 없었다. 그런데...

 

이게 되네...

 

 

3. Issue의 원인

 

문제가 해결은 되었지만, 단순히 "문제가 해결되었습니다! 여러분 안녕!"에서 끝날 것이라면 포스팅 작성을 시작하지도 않았다. 불행히도 필자의 이러한 습성과 더불어, Stack Overflow에서 답변을 달아준 친구가 이 이슈의 원인에 대한 어떠한 언급도 없었기 때문에 지금도 잠에 들지 못하고 포스팅을 작성하고 있는 것이고 말이다(잘 정리해 둔 글이 나중에 피가 되고 살이 될 것이라 믿는다).

 

어쨌든 왜 firewall-cmd의 rich rule만 shell=True 옵션으로 subprocess.run() 함수를 실행해야하는지 검색을 진행해보았다. 완전하지는 않지만, 그럴듯한 이유를 만들수 있는 자료 하나를 발견했다.

 

 

먼저, 필자가 서두에 작성한 List 형태의 명령어로 실행하는 방식은 python의 os 모듈에 존재하는 *os._execvp() 함수가 명령어와 연관된 하나의 프로그램을 하위 프로세스로 실행할 수 있도록 Sequence를 명시하는 것이다. 반면, String 형태의 명령어로 옵션 shell=True를 부여하여 실행하는 방식은, 말 그대로 Shell이 직접 명시된 명령어를 수행하도록 만드는 것이다.

 

*  os._execpv() 함수는 os.execpv()가 실행하는 함수다. 

 

잠깐 firewall-cmd의 이야기를 해보자. CentOS 7부터 등장한 firewall-cmd는 이전의 iptables의 복잡한 정책 적용 방식을 개선하기 위한 것이다. 그러나 쉽게 사용할 수 있게 된 만큼, 복잡한 정책을 만드는것에는 한계가 있기 때문에 firewall-cmd는 rich rule이라 불리는, iptables에서 사용한 방식으로 정책을 작성할 수 있는 기능을 추가해놓았다.

 

출처: https://firewalld.org/documentation/architecture.html

 

위의 그림은 firewall-cmd 명령어의 Architecture다. 잘 보면 backend 부분에 iptables와 ip6tables와 같이, 오래 전의 linux에서 동작하던 방화벽 시스템 외에도 nft가 고스란히 살아남아 있는 것을 볼 수 있는데, 이들의 존재로 인해 firewall-cmd에서도 rich rule을 사용하여 정교한 정책을 만들 수 있는 것이다.

 

다시 본론으로 돌아와보자. firewall-cmd로 rich rule을 삭제/생성하는 프로세스는 절대 하나의 프로그램(firewall-cmd)로만 동작하지 않는다. 당연히 iptables가 연관되어 있다. 그런데, rich rule을 제어하는 명령어를 subprocess.run() 함수에 list 형태로 실행하게 된다면, 명령어를 실행하게 되는 os._execpv() 는 이렇게 생각할 것이다.

 

"얌마, 너 firewall-cmd에서 실행하라고 레시피 던져줬는데, 뒤에 있는 옵션은 iptables나 nft랑 관련있잖아! 난 이딴거 몰라!"

 

그래서 rich rule과 연관된 명령어의 경우, shell=True 옵션을 적용하여 직접적으로 Shell이 명령어를 실행하도록 만들어야 한다는 것이다. Shell은 firewall-cmd도 알고 iptables도 알기 때문에...

 


 

Fin.

 

 

 

* Reference:

- https://stackoverflow.com/questions/49185476/add-rich-rules-in-firewalld-using-python3-loop

- https://stackoverflow.com/questions/23520775/python-subprocess-command-as-list-not-string

 

 

반응형

댓글