Byeo

bind system call 3 (__inet_bind - 2) 본문

프로그래밍 (Programming)/네트워크 스택

bind system call 3 (__inet_bind - 2)

BKlee 2024. 5. 1. 20:35
반응형

이전 포스트

https://byeo.tistory.com/entry/bind-system-call-2-inetbind

 

bind system call 2 (inet_bind)

이전 포스트 (bind system call 1: __sys_bind)https://byeo.tistory.com/entry/bind-system-call-1 bind system call 1 (__sys_bind)BSD socket API에서 socket을 생성한 뒤에는 server-side 측에서 bind를 실행합니다. bind는 나의 ip:port를 s

byeo.tistory.com

 

현재까지의 흐름도

__inet_bind를 이어서 탐색해 봅시다.

 

4. __inet_bind

3) __inet_bind로 돌아와서

3-1) 주소 검사

// net/ipv4/af_inet.c/__inet_bind() :494

	err = -EADDRNOTAVAIL;
	if (!inet_can_nonlocal_bind(net, inet) &&
	    addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
	    chk_addr_ret != RTN_LOCAL &&
	    chk_addr_ret != RTN_MULTICAST &&
	    chk_addr_ret != RTN_BROADCAST)
		goto out;

 

sysctl에서 ip_nonlocal_bind라는 항목이 있습니다.

 

내 host가 소지하지 않은 주소를 bind 할 수 있는지 설정하는 항목입니다. 보통은 0입니다.

 

따라서, 해당 설정이 false이면서, INADDR_ANY가 아니면서, RTN_LOCAL의 값이 아니면서, RTN_MULTICAST가 아니면서, RTN_BROADCAST가 아닌 경우, 내 local이 소지하지 않은 주소라고 판단하고 -EADDRNOTAVAIL error를 내뱉으며 bind를 종료합니다.

 

3-2) 1024 미만의 port bind에 대한 권한 검사

지금까지 주소의 유효성을 테스트했다면, 이제 포트에 대한 검사를 시작할 차례입니다!

// net/ipv4/af_inet.c/__inet_bind() :502

	snum = ntohs(addr->sin_port);
	err = -EACCES;
	if (!(flags & BIND_NO_CAP_NET_BIND_SERVICE) &&
	    snum && inet_port_requires_bind_service(net, snum) &&
	    !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
		goto out;

 

사용자가 htons()로 변환했던 port를 다시 host endian으로 변경해 줍니다.

 

flags는 BIND_WITH_LOCK으로 넘어왔습니다. 따라서 if문의 1번째 조건은 true, snum도 양수이므로 true, inet_port_requires_bind_service는 snum 값이 1024 미만인지를 검사합니다. 상황에 따라 true or false. ns_capable은 namespace에 권한이 있는지를 검사합니다.

	// net/ipv4/af_inet.c/inet_init_net() :1861
    
    net->ipv4.sysctl_ip_prot_sock = PROT_SOCK;

 

// include/net/sock.h :1438

/* Sockets 0-1023 can't be bound to unless you are superuser */
#define PROT_SOCK	1024

 

요약하여, bind port가 1024 미만의 값으로 요청된 경우에는 권한이 있는지를 확인하는 코드라고 보면 되겠습니다. 이는 1024 미만에 reserved port가 많기 때문입니다.

 

 

3-3) bind lock

// net/ipv4/af_inet.c/__inet_bind() :516

	if (flags & BIND_WITH_LOCK)
		lock_sock(sk);

 

BIND_WITH_LOCK flag로 내려왔으니 lock을 겁니다.

 

3-4) active socket check

	/* Check these errors (active socket, double bind). */
	err = -EINVAL;
	if (sk->sk_state != TCP_CLOSE || inet->inet_num)
		goto out_release_sock;

 

 

socket status가 TCP_CLOSE가 아닌 경우 -EINVAL error를 return 합니다. inet->inet_num이 0이 아닐때에도 -EINVAL을 return합니다. inet->inet_num은 socket create당시 SOCK_RAW type인 경우에 protocol 번호를 갖고 있습니다. (따라서 SOCK_STREAM인 TCP의 경우에서는 0)

 

3-5) source address 대입

	inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
	if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
		inet->inet_saddr = 0;  /* Use device */

 

inet의 inet_rcv_saddr, inet->inet_saddr에 주소 값을 대입합니다. multicast 거나 broadcast type의 주소인 경우에는 0을 대입합니다.

 

3-6) bind 가능 여부 확인

	/* Make sure we are allowed to bind here. */
	if (snum || !(inet->bind_address_no_port ||
		      (flags & BIND_FORCE_ADDRESS_NO_PORT))) {
		if (sk->sk_prot->get_port(sk, snum)) {
			inet->inet_saddr = inet->inet_rcv_saddr = 0;
			err = -EADDRINUSE;
			goto out_release_sock;
		}
		if (!(flags & BIND_FROM_BPF)) {
			err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
			if (err) {
				inet->inet_saddr = inet->inet_rcv_saddr = 0;
				goto out_release_sock;
			}
		}
	}

 

snum은 보통 양수일 테니 첫 if 조건은 true입니다. 그러면 두 번째 if문에서 sk->sk_prot->get_port를 실행합니다. sk->sk_prot는 tcp_prot 구조체로,  이 아래에 get_port가 있습니다.

 

// net/ipv4/tcp_ipv4.c:3074

struct proto tcp_prot = {
	.name			= "TCP",
    
    	...
    	.get_port		= inet_csk_get_port,
    
}

 

tcp 구조체지만 호출은 icsk에 있는 함수를 호출하는군요.

3-7) inet_csk_get_port

// net/ipv4/inet_connection_sock.c/inet_csk-get_port :358

/* Obtain a reference to a local port for the given sock,
 * if snum is zero it means select any available local port.
 * We try to allocate an odd port (and leave even ports for connect())
 */
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
	bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
	struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
	int ret = 1, port = snum;
	struct inet_bind_hashbucket *head;
	struct net *net = sock_net(sk);
	struct inet_bind_bucket *tb = NULL;
	int l3mdev;

	l3mdev = inet_sk_bound_l3mdev(sk);

	if (!port) {
		head = inet_csk_find_open_port(sk, &tb, &port);
		if (!head)
			return ret;
		if (!tb)
			goto tb_not_found;
		goto success;
	}
	head = &hinfo->bhash[inet_bhashfn(net, port,
					  hinfo->bhash_size)];
	spin_lock_bh(&head->lock);
	inet_bind_bucket_for_each(tb, &head->chain)
		if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev &&
		    tb->port == port)
			goto tb_found;
tb_not_found:
	tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
				     net, head, port, l3mdev);
	if (!tb)
		goto fail_unlock;
tb_found:
	if (!hlist_empty(&tb->owners)) {
		if (sk->sk_reuse == SK_FORCE_REUSE)
			goto success;

		if ((tb->fastreuse > 0 && reuse) ||
		    sk_reuseport_match(tb, sk))
			goto success;
		if (inet_csk_bind_conflict(sk, tb, true, true))
			goto fail_unlock;
	}
success:
	inet_csk_update_fastreuse(tb, sk);

	if (!inet_csk(sk)->icsk_bind_hash)
		inet_bind_hash(sk, tb, port);
	WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
	ret = 0;

fail_unlock:
	spin_unlock_bh(&head->lock);
	return ret;
}
EXPORT_SYMBOL_GPL(inet_csk_get_port);

 

 

가장 먼저 inet_sk_biound_l3mdev를 호출합니다. 그리고 port가 0이라면 port 번호를 찾아옵니다.

	l3mdev = inet_sk_bound_l3mdev(sk);

	if (!port) {
		head = inet_csk_find_open_port(sk, &tb, &port);
		if (!head)
			return ret;
		if (!tb)
			goto tb_not_found;
		goto success;
	}

 

일반적으로  bind는 port 번호를 지정해서 호출합니다. 단, 만약 0으로 호출한 경우에는 일반적으로 bind는 짝수 번호를 부여하도록 다고 합니다. (주석을 참고해 보면 connect() 함수가 홀수를 사용하도록 시도합니다.)

 

	head = &hinfo->bhash[inet_bhashfn(net, port,
					  hinfo->bhash_size)];

 

inet_bhashfn은 입력된 lport의 값에 net_hash_mix value를 더한 뒤, 다시 hinfo->bhash_size bit만큼 마스킹해서 반환합니다. bhash는 bind hash의 약자로 보입니다. 반대로 lhash (listen hash)도 존재합니다.

// include/net/inet_hashtables.h/inet_bhashfn() :224

static inline u32 inet_bhashfn(const struct net *net, const __u16 lport,
			       const u32 bhash_size)
{
	return (lport + net_hash_mix(net)) & (bhash_size - 1);
}

 

각각 값이 얼마일지 궁금해서 한 번 출력해 보았습니다.

 

함수의 결괏값은 990, 제가 생성한 bind port는 12345, hash value는 -1931570267, bhash_size는 16384였습니다.

 

hinfo는 struct inet_hashinfo이며, bhash의 type은 struct inet_bind_hash_bucket입니다. 이 구조체는 다음과 같습니다.

// include/net/inet_hashtables.h :101

struct inet_bind_hashbucket {
	spinlock_t		lock;
	struct hlist_head	chain;
};

 

이 구조체에 대한 포인터를 가지고 다음을 실행합니다. 

	spin_lock_bh(&head->lock);
	inet_bind_bucket_for_each(tb, &head->chain)
		if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev &&
		    tb->port == port)
			goto tb_found;

 

bottom half spin_lock을 얻습니다. 그리고 inet_bind_hashbucket의 chain들을 돌기 시작합니다. 순회하는 포인터는 tb에 저장되며, type은 inet_bind_bucket입니다.

 

// include/net/inet_hashtables.h :76

struct inet_bind_bucket {
	possible_net_t		ib_net;
	int			l3mdev;
	unsigned short		port;
	signed char		fastreuse;
	signed char		fastreuseport;
	kuid_t			fastuid;
#if IS_ENABLED(CONFIG_IPV6)
	struct in6_addr		fast_v6_rcv_saddr;
#endif
	__be32			fast_rcv_saddr;
	unsigned short		fast_sk_family;
	bool			fast_ipv6_only;
	struct hlist_node	node;
	struct hlist_head	owners;
};

 

여기서 namespace가 같고, l3mdev가 같으며 port가 같다면 이미 존재하는 포트가 되어서 tb_found로 빠지고, 찾지 못한다면 밑을 이어서 실행합니다.

 

 

3-7-1) tb_not_found

port가 사용 중이지 않습니다. 따라서 inet_bind_bucket_create를 호출합니다.

 

tb_not_found:
	tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
				     net, head, port, l3mdev);
	if (!tb)
		goto fail_unlock;

 

해당 함수는 새로운 bucket을 위해 공간을 할당받고 값을 대입하는 함수입니다.

// net/ipv4/inet_hashtables.c/inet_bind_bucket_create() :60

/*
 * Allocate and initialize a new local port bind bucket.
 * The bindhash mutex for snum's hash chain must be held here.
 */
struct inet_bind_bucket *inet_bind_bucket_create(struct kmem_cache *cachep,
						 struct net *net,
						 struct inet_bind_hashbucket *head,
						 const unsigned short snum,
						 int l3mdev)
{
	struct inet_bind_bucket *tb = kmem_cache_alloc(cachep, GFP_ATOMIC);

	if (tb) {
		write_pnet(&tb->ib_net, net);
		tb->l3mdev    = l3mdev;
		tb->port      = snum;
		tb->fastreuse = 0;
		tb->fastreuseport = 0;
		INIT_HLIST_HEAD(&tb->owners);
		hlist_add_head(&tb->node, &head->chain);
	}
	return tb;
}

 

inet_bind_bucket type의 공간을 할당받아서 namespace, l3mdev, port 등을 설정해 놓습니다. 

 

3-7-2) tb_found

 

tb_found:
	if (!hlist_empty(&tb->owners)) {
		if (sk->sk_reuse == SK_FORCE_REUSE)
			goto success;

		if ((tb->fastreuse > 0 && reuse) ||
		    sk_reuseport_match(tb, sk))
			goto success;
		if (inet_csk_bind_conflict(sk, tb, true, true))
			goto fail_unlock;
	}

그다음으로는 tb_found가 있습니다. 하지만 조건문에서 hlist_empty가 true고 (INIT_HLIST_HEAD(&tb->onwers)는 NULL값 설정), 따라서 아래는 사실 실행하지 않습니다. 그래도 코드를 한 번 봐보겠습니다.

 

1. sk_reuse가 SK_FORCE_REUSE면 무조건 bind success. (단, 커널을 찾아보니 sk->sk_reuse = SK_FORCE_REUSE로 값을 지정하는 코드는 tcp_setsockopt에서 TCP_REPAIR를 TCP_REPAIR_ON으로 설정하는 곳 밖에 없었습니다.)

2. tb->fastreuse가 0보다 크고 reuse (bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;)가 true거나 sk_reuseport_match가 true라면 bind success.

  •  sk->sk_reuse가 true로 설정되는 경우는 setsockopt을 통해서 SO_REUSEADDR을 설정한 경우입니다. SO_REUSEADDR을 설정하면 sk->sk_reuse가 SK_CAN_REUSE값으로 변경됩니다.
  •  sk->sk_state는 socket의 상태를 나타냅니다. 현재 socket의 상태가 LISTEN만 아닌 경우에 reuse가 가능합니다.
  • sk_reuseport_match는 기존에 있던 socket (tb)의 address와 현재 새로이 bind 되는 socket의 socketoption과 주소를 검사합니다. 주소 검사 시에 wildcard (INADDR_ANY)도 고려합니다. tb_found이기 때문에 두 socket의 port는 이미 동일합니다.

3. 여기까지 통과하지 못했다면, inet_csk_bind_conflict 함수를 통해서 socket과 기존 socket(tb)을 sock 수준에서 비교하고, 여전히 불가능하다면 inet_csk_get_port 함수를 1로 return 합니다.

 

3-7-3) success

	inet_csk_update_fastreuse(tb, sk);

	if (!inet_csk(sk)->icsk_bind_hash)
		inet_bind_hash(sk, tb, port);
	WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
	ret = 0;

 

마지막 작업을 처리합니다.

 

inet_csk_update_fastreuse는 socket의 option들을 조사하여 inet_bind_bucket 구조체에 값을 저장합니다.

예를 들어서 현재 bind중인 socket에 setsockopt으로 SO_REUSEADDR이 설정되어 있었다면 sk->sk_reuse는 true일 것입니다. 이를 tb->fastreuse에 보관합니다. SO_REUSEPORT도 마찬가지로 설정되어 있었다면 추가로 작업을 처리합니다.

// net/ipv4/inet_connection_socket.c/inet_csk_update_fastreuse() :307

void inet_csk_update_fastreuse(struct inet_bind_bucket *tb,
			       struct sock *sk)
{
	kuid_t uid = sock_i_uid(sk);
	bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;

	if (hlist_empty(&tb->owners)) {
		tb->fastreuse = reuse;
		if (sk->sk_reuseport) {
			tb->fastreuseport = FASTREUSEPORT_ANY;
			tb->fastuid = uid;
			tb->fast_rcv_saddr = sk->sk_rcv_saddr;
			tb->fast_ipv6_only = ipv6_only_sock(sk);
			tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
			tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
		} else {
			tb->fastreuseport = 0;
		}
	} else {
		if (!reuse)
			tb->fastreuse = 0;
		if (sk->sk_reuseport) {
			/* We didn't match or we don't have fastreuseport set on
			 * the tb, but we have sk_reuseport set on this socket
			 * and we know that there are no bind conflicts with
			 * this socket in this tb, so reset our tb's reuseport
			 * settings so that any subsequent sockets that match
			 * our current socket will be put on the fast path.
			 *
			 * If we reset we need to set FASTREUSEPORT_STRICT so we
			 * do extra checking for all subsequent sk_reuseport
			 * socks.
			 */
			if (!sk_reuseport_match(tb, sk)) {
				tb->fastreuseport = FASTREUSEPORT_STRICT;
				tb->fastuid = uid;
				tb->fast_rcv_saddr = sk->sk_rcv_saddr;
				tb->fast_ipv6_only = ipv6_only_sock(sk);
				tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
				tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
			}
		} else {
			tb->fastreuseport = 0;
		}
	}
}

 

다음으로, inet_csk(sk)->icsk_bind_hash는 처음 생성한 socket의 경우에 NULL일 것이므로, sock* sk의 icsk_bind_hash에 tb를 설정하고, inet_num도 port번호로 초기화합니다.

// net/ipv4/inet_hashtables.c/inet_bind_hash() :95

void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
		    const unsigned short snum)
{
	inet_sk(sk)->inet_num = snum;
	sk_add_bind_node(sk, &tb->owners);
	inet_csk(sk)->icsk_bind_hash = tb;
}

 

마지막으로 값이 잘 입력됐는지 WARN_ON(inet_csk(sk)->icsk_bind_hash != tb)에서 확인하고 0을 return 합니다.

 

4) __inet_bind로 돌아와서 2

남은 코드는 다음과 같습니다.

// net/ipv4/af_inet.c/__inet_bind() :528

	/* Make sure we are allowed to bind here. */
	if (snum || !(inet->bind_address_no_port ||
		      (flags & BIND_FORCE_ADDRESS_NO_PORT))) {
		if (sk->sk_prot->get_port(sk, snum)) {
			inet->inet_saddr = inet->inet_rcv_saddr = 0;
			err = -EADDRINUSE;
			goto out_release_sock;
		}
		if (!(flags & BIND_FROM_BPF)) {
			err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
			if (err) {
				inet->inet_saddr = inet->inet_rcv_saddr = 0;
				goto out_release_sock;
			}
		}
	}

	if (inet->inet_rcv_saddr)
		sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
	if (snum)
		sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
	inet->inet_sport = htons(inet->inet_num);
	inet->inet_daddr = 0;
	inet->inet_dport = 0;
	sk_dst_reset(sk);
	err = 0;
out_release_sock:
	if (flags & BIND_WITH_LOCK)
		release_sock(sk);
out:
	return err;
}

 

sk->sk_prot->get_port(sk, snum)을 열심히 보고 있었는데요, 바로 위에서 적힌 것처럼 오류가 없다면 0을, bind가 불가능하다면 1을 return 합니다.

여기서 1을 받았다면 sys_bind 함수 자체는 -EADDRINUSE를 return을 하게 되고 이 오류가 우리가 많이 보던 Address already in use입니다.

 

inet->inet_rcv_saddr와 snum은 모두 설정되었으니 sk->sk_userlocks은 SOCK_BINDADDR_LOCK과 SOCK_BINDPORT_LOCK을 모두 갖게 됩니다.

 

그리고 inet->inet_sport, inet->inet_daddr, inet->inet_dport에 값을 설정합니다. inet_sport에는 htons 엔디안으로 들어감을 유의해야 합니다!

 

마지막으로 sk_dst_reset(sk)을 호출합니다.

// include/net/sock.h/sk_dst_reset() :2056

static inline void
sk_dst_reset(struct sock *sk)
{
	sk_dst_set(sk, NULL);
}

 

 

그리고 이는 다시 sk_dst_set을 호출합니다.

// include/net/sock.h/sk_dst_set() :2039

static inline void
sk_dst_set(struct sock *sk, struct dst_entry *dst)
{
	struct dst_entry *old_dst;

	sk_tx_queue_clear(sk);
	sk->sk_dst_pending_confirm = 0;
	old_dst = xchg((__force struct dst_entry **)&sk->sk_dst_cache, dst);
	dst_release(old_dst);
}

 

 

여기까지 온다면 이제 __inet_bind()는 lock을 해제, 0을 return 합니다.

 

 

5. __sys_bind

__inet_bind()를 0을 return 하면 inet_bind()도 0을 return 하고 __sys_bind()로 돌아오게 됩니다.

 

// net/socket.c/__sys_bind() :1679

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		err = move_addr_to_kernel(umyaddr, addrlen, &address);
		if (!err) {
			err = security_socket_bind(sock,
						   (struct sockaddr *)&address,
						   addrlen);
			if (!err)
				err = sock->ops->bind(sock,
						      (struct sockaddr *)
						      &address, addrlen);
		}
		fput_light(sock->file, fput_needed);
	}
	return err;
}

 

우리는 지금까지 err = sock->ops->bind를 다녀왔었는데요, 이제 마지막으로는 fput_light를 실행하고 시스템콜을 return 합니다.

fput_light는 sockfd_lookup_light에서 reference count를 1 증가시켰던 fdget에 대응하여 다시 1을 감소시키는 것이라고 보면 되겠습니다.

 

이렇게 bind가 끝나게 됩니다!

최종 흐름도

반응형
Comments