How Linux Determines the Client Port for TCP Connections
On Linux, the client port used in a TCP connection is selected either by the kernel during the connect system call—randomly scanning the ip_local_port_range while respecting reserved ports—or by a prior bind call, with detailed kernel functions such as inet_hash_connect and __inet_hash_connect governing the process.
Hello everyone, I'm Feige! In a TCP connection, the client must determine a local port before initiating the handshake with the server. This article explains how Linux selects that client port, why many common networking issues are related to this process, and how to troubleshoot them.
What causes the "Cannot assign requested address" error?
Can a client port be used for two simultaneous TCP connections?
We start with a minimal two‑line C example:
int main(){
fd = socket(AF_INET,SOCK_STREAM, 0);
connect(fd, ...);
...
}1. Creating a socket
Clients call socket(AF_INET,SOCK_STREAM,0) to obtain a file descriptor. Internally the kernel creates several objects (file, socket, sock) that are linked together.
These objects are allocated during the socket system call. The entry‑point code is shown below:
//file: net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol){
// create socket, sock objects and initialize
sock_create(family, type, protocol, &sock);
// allocate fd and create file object
sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
...
}2. connect initiates the connection
The connect call is split into several subsections for clarity.
2.1 connect call chain
When a user program calls connect, the kernel executes the following syscall:
//file: net/socket.c
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
int, addrlen){
struct socket *sock;
// look up the socket object from the user fd
sock = sockfd_lookup_light(fd, &err, &fput_needed);
// invoke the socket's connect operation
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
sock->file->f_flags);
...
}The ops->connect pointer resolves to inet_stream_connect:
//file: ipv4/af_inet.c
int inet_stream_connect(struct socket *sock, ...){
...
__inet_stream_connect(sock, uaddr, addr_len, flags);
}
int __inet_stream_connect(struct socket *sock, ...){
struct sock *sk = sock->sk;
switch (sock->state) {
case SS_UNCONNECTED:
err = sk->sk_prot->connect(sk, uaddr, addr_len);
sock->state = SS_CONNECTING;
break;
}
...
}At this point the socket state is SS_UNCONNECTED, so the tcp_v4_connect function is eventually called.
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len){
// set state to SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
// dynamically choose a local port
err = inet_hash_connect(&tcp_death_row, sk);
// build and send the SYN packet
err = tcp_connect(sk);
}2.2 Selecting an available port
The port selection happens inside inet_hash_connect:
//file: net/ipv4/inet_hashtables.c
int inet_hash_connect(struct inet_timewait_death_row *death_row,
struct sock *sk){
return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),
__inet_check_established, __inet_hash_nolisten);
} inet_sk_port_offset(sk)generates a pseudo‑random offset based on the destination address, while __inet_check_established is used to avoid collisions with already established connections.
The core of the algorithm is in __inet_hash_connect:
//file: net/ipv4/inet_hashtables.c
int __inet_hash_connect(...){
const unsigned short snum = inet_sk(sk)->inet_num; // bound port, if any
inet_get_local_port_range(&low, &high);
remaining = (high - low) + 1;
if (!snum) {
for (i = 1; i <= remaining; i++) {
port = low + (i + offset) % remaining;
...
}
}
}If the socket has not been bound ( snum == 0), the kernel reads the configurable range net.ipv4.ip_local_port_range (default 32768‑61000, i.e., 28 232 ports). The loop starts at a random offset and scans the whole range until a free port is found.
The default range is 32768‑61000; you can enlarge it by modifying net.ipv4.ip_local_port_range if needed.
During each iteration the kernel checks:
Whether the port is listed in net.ipv4.ip_local_reserved_ports ( inet_is_reserved_local_port).
Whether the port already appears in the hash table of used ports ( hinfo->bhash).
//file: net/ipv4/inet_hashtables.c
for (i = 1; i <= remaining; i++) {
port = low + (i + offset) % remaining;
// skip reserved ports
if (inet_is_reserved_local_port(port))
continue;
// look through the hash bucket of used ports
head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];
inet_bind_bucket_for_each(tb, &head->chain) {
if (net_eq(ib_net(tb), net) && tb->port == port) {
if (!check_established(death_row, sk, port, &tw))
goto ok;
}
}
// not found – the port is free
goto ok;
}
return -EADDRNOTAVAIL;
ok:
...If you want to keep certain ports from being allocated, add them to net.ipv4.ip_local_reserved_ports .
If no free port exists, the kernel returns -EADDRNOTAVAIL, which appears to user programs as the “Cannot assign requested address” error.
/* Cannot assign requested address */
#define EADDRNOTAVAIL 992.3 What happens when a port is already in use?
Even if a port appears in the hash table, it may still be usable. The function __inet_check_established examines the ESTABLISHED‑state hash table to see whether the full four‑tuple (source IP, source port, destination IP, destination port) matches an existing connection.
//file: net/ipv4/inet_hashtables.c
static int __inet_check_established(struct inet_timewait_death_row *death_row,
struct sock *sk, __u16 lport,
struct inet_timewait_sock **twp){
struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
sk_nulls_for_each(sk2, node, &head->chain) {
if (sk2->sk_hash != hash)
continue;
if (likely(INET_MATCH(sk2, net, acookie, saddr, daddr, ports, dif)))
goto not_unique;
}
return 0; // unique – port can be used
not_unique:
return -EADDRNOTAVAIL;
}The macro INET_MATCH compares the four‑tuple and a few additional fields:
// include/net/inet_hashtables.h
#define INET_MATCH(__sk, __net, __cookie, __saddr, __daddr, __ports, __dif) \
((inet_sk(__sk)->inet_portpair == (__ports)) && \
(inet_sk(__sk)->inet_daddr == (__saddr)) && \
(inet_sk(__sk)->inet_rcv_saddr == (__daddr)) && \
(!(__sk)->sk_bound_dev_if || (__sk)->sk_bound_dev_if == (__dif)) && \
net_eq(sock_net(__sk), (__net)))If the four‑tuple matches exactly, the port is considered occupied; otherwise the same local port can be reused for a different remote endpoint. Consequently, a single client can have many more than 65 535 concurrent connections as long as the remote endpoints differ.
A client machine’s maximum number of connections is not limited to 65 535; with enough distinct servers, millions of connections are possible.
2.4 Sending the SYN packet
After a free port is chosen, tcp_v4_connect calls tcp_connect to build and transmit the SYN:
//file: net/ipv4/tcp_output.c
int tcp_connect(struct sock *sk){
// allocate an skb and set it as SYN
buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);
tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
// queue the skb for transmission
tcp_connect_queue_skb(sk, buff);
// actually send the SYN (or SYN+data for fastopen)
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
// start the retransmission timer
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
}The steps are:
Allocate an skb and mark it as a SYN packet.
Place it on the socket’s write queue.
Transmit the packet.
Start a timer to handle possible retransmissions.
3. How bind selects a port
If a socket is bound before connect, the kernel skips the automatic selection and uses the port stored in inet_sk(sk)->inet_num. Binding can be done with an explicit port or with port 0, which triggers its own selection algorithm.
Binding a client socket is generally discouraged because it interferes with the kernel’s normal port‑selection logic.
//file: net/ipv4/af_inet.c
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len){
struct sock *sk = sock->sk;
// user‑supplied port number
snum = ntohs(addr->sin_port);
// reject privileged ports unless the process has CAP_NET_BIND_SERVICE
if (snum && snum < PROT_SOCK &&
!ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
goto out;
// try to allocate the requested port
if (sk->sk_prot->get_port(sk, snum)) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
...
}When snum == 0, the helper inet_csk_get_port runs a similar random‑start scan over ip_local_port_range, skipping reserved ports and checking for conflicts via bind_conflict:
//file: net/ipv4/inet_connection_sock.c
int inet_csk_get_port(struct sock *sk, unsigned short snum){
if (!snum) {
inet_get_local_port_range(&low, &high);
remaining = (high - low) + 1;
smallest_rover = rover = net_random() % remaining + low;
do {
if (inet_is_reserved_local_port(rover))
goto next_nolock;
head = &hashinfo->bhash[inet_bhashfn(net, rover,
hashinfo->bhash_size)];
inet_bind_bucket_for_each(tb, &head->chain)
if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
snum = rover;
goto tb_found;
}
} while (--remaining > 0);
}
...
}The bind‑conflict check only looks at sockets that are still in the BIND state, so a port that has been used once will not be reused by another bind operation (unless the previous socket is closed).
4. Conclusion
The client port is determined in two possible places:
During the connect system call, the kernel randomly walks ip_local_port_range, skips ports listed in ip_local_reserved_ports, and uses inet_hash_connect to find a free slot. If no slot is found, the user sees the “Cannot assign requested address” error, which often indicates that the configured port range is too small.
If the socket was bound beforehand, the port chosen (or explicitly supplied) by bind is used, and the automatic selection in connect is bypassed. Binding a client socket is generally not recommended.
Because the kernel starts its search from a random offset, a well‑sized ip_local_port_range keeps the lookup fast. When the range becomes exhausted, the kernel must iterate many times, increasing CPU usage for each connect call. Therefore, it is advisable to configure a sufficiently large port range in advance.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Refining Core Development Skills
Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
