Netlink socket 으로 네트워크 인터페이스 모니터링 하기 with GO

Hamji·2022년 5월 24일
0

본 게시글은 개얕은 실력으로 아래의 출처를 참고하여 작성하였습니다.
출처

개요


Netlink? 얘가 뭘까?

Netlink 구글을 키고 man netlink 를 치면 친절하게 설명이 하나가 나온다.

Netlink is used to transfer information between the kernel and user-space processes.

Kernel과 User-space process 간 정보 교환시 사용되는 것이라고 한다.
또한 standard socket-based interface 로 이루어져 있다는 것을 알 수 있다.
그냥 user-space 와 kernel 간 통신을 위한 표준 소켓 기반 API 라 생각하자

사실 이론적인 글을 쓰려고 이 글을 작성한 것은 아니고 내 얕고 좁은 지식에 도움이 되었다 생각해서 기록을 남기려고 썼다.

최근 회사에서 스크립트 작성 도중, 네트워크 인터페이스에 virtual ip 리소스가 붙었다가 떼어졌다 하는걸 감시하고 그에 대한 이벤트를 처리하는 로직을 넣어야 할 일이 있었다.
python 으로 후다닥 작성하였는데 당시 내가 작성한 것은 아래의 방식과 같다.

1. psutil을 이용하여 일정 시간마다 체크하는 방식
2. 특정 명령어(비밀)를 os.popen을 이용하여 사용하여 결과 값을 파싱하는 방식

이 두가지 방식으로 작성하고, 회의를 들어갔었다.
사실 좋은 방식은 아니라고 생각은 들었다. 왜냐하면 둘다 특정 시간마다 loop 을 돌면서 실행하는 방법인데, 특정 시간마다 계속 연산을 해야한다는 점이 매우 좋지 않은 방법이라 생각했다.

심지어 이 특정 명령어는 실행시 1초~ 1.5초나 걸리는 관계로 감시 interval을 0.1 초씩 주어도 이 명령어 실행하는데 1초 씩이나 걸리니 interval을 준게 의미가 없을 정도였다.

그러던 도중 옆자리 상사분이 raw socket으로 netlink bind 걸고 멀티플렉싱으로 처리하면 별도의 부하없이 잘 처리되지 않냐고 말씀하셨다.

들으면서 처음에 raw socket..? low socket..? netlink..? 그게 뭐지..? 라는 생각이 들면서 아리송 하였다.

그래서 한번 공부해서 적용해 보겠다고 하고 찾아보았다.

https://stackoverflow.com/questions/7225888/how-can-i-monitor-the-nic-statusup-down-in-a-c-program-without-polling-the-ker

모르겠을때 보는 킹갓 오버 플로우를 보니깐 좋은 예제가 있어서 링크를 남긴다.

위의 글에서는 raw socket에 netlink 바인딩 이후 select를 이용해 이벤트가 일어나면 메시지를 RECV 해와서 파싱해서 처리하는 그런 코드라고 나는 파악했다 (아니면 말좀...)

뭐 이런 방법을 알고 python에 적용하여 그대로 처리하고 싶었으나 이런 저런 이유(내부사정) 으로 그냥 interval 지나면 체크하는 방식으로 진행하기로 하였다. ㅡ,ㅡ

참고로 raw socket이란 아래의 설명과 같다. OSI7 layer를 아시는 분이라면 바로 알아들으실듯 하다... 본 글은 raw socket에 대해서 쓴글이 아니니까 아래의 설명으로 넘어가겠다..

raw 소켓은 어느 특정한 프로토콜 용의 전송 계층 포매팅 없이 인터넷 프로토콜 패킷을 직접적으로 주고 받게 해주는 인터넷 소켓이다 - 위키백과

go


회사일은 그렇게 진행 되었지만, 이런 개념은 굉장히 좋은 방법 중 하나 같아서 GOLANG 코드 분석과 같이 실행해볼까한다.

맨 위의 출처를 들어가보면 Network의 address 변화를 모니터링하고 싶어하는 사람의 질문글이다.

답변 코드를 보면

  package main

import (
	"fmt"
	"log"
	"syscall"
)

// main function
func main() {
	netlink, err := ListenNetlink()
	if err != nil {
		log.Printf("[ERR] Could not create netlunk listener: %v", err)
		return
	}

	for {
		msgs, err := netlink.ReadMsgs()
		if err != nil {
			log.Printf("[ERR] Could not read netlink: %v", err)
		}

		for _, msg := range msgs {
			if _, ok := msg.(*syscall.InterfaceAddrMessage); ok {
				log.Printf("address change!")
			}
		}
	}

}

// 구조체
type NetlinkListener struct {
	fd int
}

func ListenNetlink() (*NetlinkListener, error) {
	s, err := syscall.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
	if err != nil {
		return nil, fmt.Errorf("socket : %s", err)
	}
	return &NetlinkListener{fd: s}, nil
}

func (l *NetlinkListener) ReadMsgs() ([]syscall.RoutingMessage, error) {
	defer func() {
		recover()
	}()

	pkt := make([]byte, 2048)
// syscall read 
	n, err := syscall.Read(l.fd, pkt)
	if err != nil {
		return nil, fmt.Errorf("read: %s", err)
	}
// 이 함수의 역할이 뭘지 모르겠다..
	msgs, err := syscall.ParseRoutingMessage(pkt[:n])
	if err != nil {
		return nil, fmt.Errorf("parse: %s", err)
	}
	return msgs, nil
}

이렇게 작성 되어 있다.
코드에 대한 분석은 난 이렇게 이해하였다.

Main

  func main() {
	netlink, err := ListenNetlink()
	if err != nil {
		log.Printf("[ERR] Could not create netlunk listener: %v", err)
		return
	}

	for {
		msgs, err := netlink.ReadMsgs()
		if err != nil {
			log.Printf("[ERR] Could not read netlink: %v", err)
		}

  
		for _, msg := range msgs {
  // msg.? 이건 뭐지...
			if _, ok := msg.(*syscall.InterfaceAddrMessage); ok {
				log.Printf("address change!")
			}
		}
	}

}

netlink 구조체 ( 멤버변수 fd를 저장할 목적) 변수를 선언하고 ReadMsgs 라는 함수를 실행하면서 읽은 메시지를 한번에 읽으며 Network address 관련 메시지라면 출력한다.

NetlinkListener

 type NetlinkListener struct {
	fd int
}

func ListenNetlink() (*NetlinkListener, error) {
	s, err := syscall.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
	if err != nil {
		return nil, fmt.Errorf("socket : %s", err)
	}
	return &NetlinkListener{fd: s}, nil
}
  
func (l *NetlinkListener) ReadMsgs() ([]syscall.RoutingMessage, error) {
	defer func() {
		recover()
	}()

	pkt := make([]byte, 2048)

	n, err := syscall.Read(l.fd, pkt)
	if err != nil {
		return nil, fmt.Errorf("read: %s", err)
	}

	msgs, err := syscall.ParseRoutingMessage(pkt[:n])
	if err != nil {
		return nil, fmt.Errorf("parse: %s", err)
	}
	return msgs, nil
}

ListenNetlink 함수는 소켓 바인드 이후 NetlinkListener
객체를 생성하여 return 해준다.

ReadMsgs 함수는 syscall.의 Read 함수를 이용하여 fd를 이용하여 Read 해 와서 읽은 만큼 syscall의 ParseRoutingMessage를 이용하여 라우팅 메시지를 파싱하고 그에대한 메시지를 return 해준다.
[ParseRoutingMessage가 뭘의미하는진튼 아리송하다... 공부해서 업뎃하겠다..]

이렇게 해서 실행해 보면서 돌려놓고 ifconfig [nic][추가할 ipaddress/mask] up/down을 하면 nic 에 임의의 ip를 붙였다 땠다 할 때마다 감지하는 것을 볼 수 있다!

2022/05/25 00:18:00 address change!
2022/05/25 00:19:21 address change!
2022/05/25 00:20:17 address change!
2022/05/25 00:20:41 address change!
2022/05/25 00:20:46 address change!

아직 golang에 익숙하지 않은 레벨이라 코드의 아래의 부분이 파악이 안된다... 파악하면 업뎃해두도록 하겠다...

if _, ok := msg.(*syscall.InterfaceAddrMessage); ok {
  
msgs, err := syscall.ParseRoutingMessage(pkt[:n]) 

두서없이 쓴글(?) 읽어주셔서 감사합니다.

profile
얕고 작은 내 지식 옹달샘

0개의 댓글