- Today
- Total
Byeo
bind system call 3 (__inet_bind - 2) 본문
이전 포스트
https://byeo.tistory.com/entry/bind-system-call-2-inetbind
현재까지의 흐름도
__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가 끝나게 됩니다!
최종 흐름도
'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글
listen system call 2 (inet_listen) (0) | 2024.05.11 |
---|---|
listen system call 1 (__sys_listen) (0) | 2024.05.11 |
bind system call 2 (__inet_bind) (0) | 2024.04.28 |
bind system call 1 (__sys_bind) (0) | 2024.04.27 |
BSD Socket API (0) | 2024.04.17 |