Skip to contentSkip to conversation

TCP connection repair (v2)

RFC PATCH In review Pavel Emelyanov 3 participants 7 messages Mar 6, 2012

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

cover TCP connection repair (v2)

Hi!

Attempt #2 with transparent TCP connection hijacking.

The idea briefly is -- introduce the "repair" mode of a TCP socket. In this mode any API call to the socket (bind/connect/sendmsg/etc.) does not result in packets sent over the network, but instead modifies the socket locally in an expected way.

I.e., the connect() in the repair mode assigns peer's credentials to the sock and just turns one into the connected state without issuing SYN-s or whatever. The bind() call on the socket under repair forcibly binds one to the desired IP and port ignoring any (potential) local conflicts (just like if everybody else has the SO_REUSEADDR set). The sendmsg() just queues data for transmission, etc.

I think, that it makes sense to have this ability in a form of non-obscure API, since the connection migration can be used not only by checkpoint/restore project, but also by various load balancing solutions. E.g., a server can accept the connection, read the app-level header out of the stream, take the balancing decision based on _it_ (rather than just TCP and/or IP header) and then pass the existing connection to another host.

Changes since v1:

* Addressed (I hope) David's concern about TCP sequences self-consistency.

The repair mode is turned on only for "static" TCP states, i.e. TCP_CLOSE and TCP_ESTABLISHED with the socket being locked. Only two sequences can be changed manually -- the write_seq and the copied_seq -- and only when the socket is in TCP_CLOSE state. The rest is maintained fully by the kernel code according to the protocol rules in connect/sendmsg/etc. calls.

* Slight API change.

Instead of two separate sockoptions for send and receive queues sequences I introduce the option which sets which queue is under repair right now and the other option for setting the sequence (as described -- works only for TCP_CLOSE state) of the queue under repair. Yes, there still two options for this, but such approach helps with socket queues repair (see below).

* Added support for queues repair.

According to the overall idea of the "repair" mode the recv/send syscalls are used for this and what they do is just peek/poke data from/to queues. The queue-under-repair set by the described option makes it possible to read from the send queue and write to the receive one. Caller is obliged to use the MSG_PEEK flag for recv in the repair mode.

Thanks,
Pavel

#1 lore raw

PATCH 1/3 tcp: Move code around

This is just the preparation patch, which makes the needed for TCP repair code ready for use.

---
 include/net/tcp.h     |    3 ++
 net/ipv4/tcp.c        |    2 +-
 net/ipv4/tcp_input.c  |   81 +++++++++++++++++++++++++++++--------------------
 net/ipv4/tcp_output.c |    4 +-
 4 files changed, 54 insertions(+), 36 deletions(-)
include/net/tcp.h +3 -0
index 8607e6a..a08e886 100644
@@ -434,6 +434,9 @@ extern struct sk_buff * tcp_make_synack(struct sock *sk, struct dst_entry *dst,
 					struct request_values *rvp);
 extern int tcp_disconnect(struct sock *sk, int flags);
 
+void tcp_connect_init(struct sock *sk);
+void tcp_finish_connect(struct sock *sk, struct sk_buff *skb);
+void tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen);
 
 /* From syncookies.c */
 extern __u32 syncookie_secret[2][16-4+SHA_DIGEST_WORDS];
net/ipv4/tcp.c +1 -1
index 22ef5f9..0e0b974 100644
@@ -916,7 +916,7 @@ int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
 	struct tcp_sock *tp = tcp_sk(sk);
 	struct sk_buff *skb;
 	int iovlen, flags, err, copied;
-	int mss_now, size_goal;
+	int mss_now = 0, size_goal;
 	bool sg;
 	long timeo;
 
net/ipv4/tcp_input.c +48 -33

106 lines not on this page · raw message

net/ipv4/tcp_output.c +2 -2
index 364784a..1db25af 100644
@@ -2561,7 +2561,7 @@ struct sk_buff *tcp_make_synack(struct sock *sk, struct dst_entry *dst,
 EXPORT_SYMBOL(tcp_make_synack);
 
 /* Do all connect socket setups that can be done AF independent. */
-static void tcp_connect_init(struct sock *sk)
+void tcp_connect_init(struct sock *sk)
 {
 	const struct dst_entry *dst = __sk_dst_get(sk);
 	struct tcp_sock *tp = tcp_sk(sk);
@@ -2616,6 +2616,7 @@ static void tcp_connect_init(struct sock *sk)
 	tp->snd_una = tp->write_seq;
 	tp->snd_sml = tp->write_seq;
 	tp->snd_up = tp->write_seq;
+	tp->snd_nxt = tp->write_seq;
 	tp->rcv_nxt = 0;
 	tp->rcv_wup = 0;
 	tp->copied_seq = 0;
@@ -2641,7 +2642,6 @@ int tcp_connect(struct sock *sk)
 	/* Reserve space for headers. */
 	skb_reserve(buff, MAX_TCP_HEADER);
 
-	tp->snd_nxt = tp->write_seq;
 	tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
 	TCP_ECN_send_syn(sk, buff);
 
Signature · 1 line
1.5.5.6
#2 lore raw

PATCH 2/3 tcp: Initial repair mode

In reply to Pavel Emelyanov

This includes (according the the previous description):

* TCP_REPAIR sockoption

This one just puts the socket in/out of the repair mode. Allowed for CAP_SYS_ADMIN and for closed/establised sockets only. When repair mode is turned off and the socket happens to be in the established state the window probe is sent to the peer to 'unlock' the connection.

* TCP_REPAIR_QUEUE sockoption

This one sets the queue which we're about to repair. The 'no-queue' is set by default.

* TCP_QUEUE_SEQ socoption

Sets the write_seq/copied_seq of a selected repaired queue. Allowed for TCP_CLOSE-d sockets only. When the socket changes its state the other seq-s are changed by the kernel according to the protocol rules (most of the existing code is actually reused).

* Ability to forcibly bind a socket to a port

The sk->sk_reuse is set to 2 denoting, that the socket is question should be bound as if all the others in the system are configured with the SO_REUSEADDR option.

* Immediate connect modification

The connect syscall initializes the connection, then directly jumps to the code which finalizes it.

* Silent close modification

The close just aborts the connection (similar to SO_LINGER with 0 time) but without sending any FIN/RST-s to peer.

---
 include/linux/tcp.h             |   14 ++++++++-
 include/net/tcp.h               |    2 +
 net/ipv4/inet_connection_sock.c |    3 ++
 net/ipv4/tcp.c                  |   63 ++++++++++++++++++++++++++++++++++++++-
 net/ipv4/tcp_ipv4.c             |   19 ++++++++++--
 net/ipv4/tcp_output.c           |   16 ++++++++--
 6 files changed, 109 insertions(+), 8 deletions(-)
include/linux/tcp.h +13 -1
index b6c62d2..4e90e6a 100644
@@ -106,6 +106,16 @@ enum {
 #define TCP_THIN_LINEAR_TIMEOUTS 16      /* Use linear timeouts for thin streams*/
 #define TCP_THIN_DUPACK         17      /* Fast retrans. after 1 dupack */
 #define TCP_USER_TIMEOUT	18	/* How long for loss retry before timeout */
+#define TCP_REPAIR		19	/* TCP sock is under repair right now */
+#define TCP_REPAIR_QUEUE	20
+#define TCP_QUEUE_SEQ		21
+
+enum {
+	TCP_NO_QUEUE,
+	TCP_RECV_QUEUE,
+	TCP_SEND_QUEUE,
+	TCP_QUEUES_NR,
+};
 
 /* for TCP_INFO socket option */
 #define TCPI_OPT_TIMESTAMPS	1
@@ -353,7 +363,9 @@ struct tcp_sock {
 	u8	nonagle     : 4,/* Disable Nagle algorithm?             */
 		thin_lto    : 1,/* Use linear timeouts for thin streams */
 		thin_dupack : 1,/* Fast retransmit on first dupack      */
-		unused      : 2;
+		repair      : 1,
+		unused      : 1;
+	u8	repair_queue;
 
 /* RTT measurement */
 	u32	srtt;		/* smoothed round trip time << 3	*/
include/net/tcp.h +2 -0
index a08e886..9f4aa4c 100644
@@ -611,6 +611,8 @@ static inline u32 tcp_receive_window(const struct tcp_sock *tp)
  */
 extern u32 __tcp_select_window(struct sock *sk);
 
+void tcp_send_window_probe(struct sock *sk);
+
 /* TCP timestamps are only 32-bits, this causes a slight
  * complication on 64-bit systems since we store a snapshot
  * of jiffies in the buffer control blocks below.  We decided
net/ipv4/inet_connection_sock.c +3 -0
index 19d66ce..92788af 100644
@@ -172,6 +172,9 @@ have_snum:
 	goto tb_not_found;
 tb_found:
 	if (!hlist_empty(&tb->owners)) {
+		if (sk->sk_reuse == 2)
+			goto success;
+
 		if (tb->fastreuse > 0 &&
 		    sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
 		    smallest_size == -1) {
net/ipv4/tcp.c +62 -1

87 lines not on this page · raw message

net/ipv4/tcp_ipv4.c +16 -3
index 94abee8..6118486 100644
@@ -137,6 +137,14 @@ int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
 }
 EXPORT_SYMBOL_GPL(tcp_twsk_unique);
 
+static int tcp_repair_connect(struct sock *sk)
+{
+	tcp_connect_init(sk);
+	tcp_finish_connect(sk, NULL);
+
+	return 0;
+}
+
 /* This will initiate an outgoing connection. */
 int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
 {
@@ -195,7 +203,8 @@ int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
 		/* Reset inherited state */
 		tp->rx_opt.ts_recent	   = 0;
 		tp->rx_opt.ts_recent_stamp = 0;
-		tp->write_seq		   = 0;
+		if (!tp->repair)
+			tp->write_seq	   = 0;
 	}
 
 	if (tcp_death_row.sysctl_tw_recycle &&
@@ -246,7 +255,7 @@ int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
 	sk->sk_gso_type = SKB_GSO_TCPV4;
 	sk_setup_caps(sk, &rt->dst);
 
-	if (!tp->write_seq)
+	if (!tp->write_seq && !tp->repair)
 		tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
 							   inet->inet_daddr,
 							   inet->inet_sport,
@@ -254,7 +263,11 @@ int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
 
 	inet->inet_id = tp->write_seq ^ jiffies;
 
-	err = tcp_connect(sk);
+	if (likely(!tp->repair))
+		err = tcp_connect(sk);
+	else
+		err = tcp_repair_connect(sk);
+
 	rt = NULL;
 	if (err)
 		goto failure;
net/ipv4/tcp_output.c +13 -3
index 1db25af..f0525d1 100644
@@ -2617,9 +2617,11 @@ void tcp_connect_init(struct sock *sk)
 	tp->snd_sml = tp->write_seq;
 	tp->snd_up = tp->write_seq;
 	tp->snd_nxt = tp->write_seq;
-	tp->rcv_nxt = 0;
-	tp->rcv_wup = 0;
-	tp->copied_seq = 0;
+
+	if (!tp->repair)
+		tp->copied_seq = 0;
+	tp->rcv_wup = tp->copied_seq;
+	tp->rcv_nxt = tp->copied_seq;
 
 	inet_csk(sk)->icsk_rto = TCP_TIMEOUT_INIT;
 	inet_csk(sk)->icsk_retransmits = 0;
@@ -2790,6 +2792,14 @@ static int tcp_xmit_probe_skb(struct sock *sk, int urgent)
 	return tcp_transmit_skb(sk, skb, 0, GFP_ATOMIC);
 }
 
+void tcp_send_window_probe(struct sock *sk)
+{
+	if (sk->sk_state == TCP_ESTABLISHED) {
+		tcp_sk(sk)->snd_wl1 = tcp_sk(sk)->rcv_nxt - 1;
+		tcp_xmit_probe_skb(sk, 0);
+	}
+}
+
 /* Initiate keepalive or window probe from timer. */
 int tcp_write_wakeup(struct sock *sk)
 {
Signature · 1 line
1.5.5.6
#3 lore raw

Re: [PATCH 2/3] tcp: Initial repair mode

Quoted 9 lines · Pavel Emelyanov

This includes (according the the previous description):

* TCP_REPAIR sockoption

This one just puts the socket in/out of the repair mode. Allowed for CAP_SYS_ADMIN and for closed/establised sockets only. When repair mode is turned off and the socket happens to be in the established state the window probe is sent to the peer to 'unlock' the connection.

Why not CAP_NET_ADMIN, or something similar?

maintainer
#4 lore raw

Re: [PATCH 2/3] tcp: Initial repair mode

From: Glauber Costa <[email protected]>
Date: Tue, 6 Mar 2012 17:11:47 +0400

Quoted 5 lines · Pavel Emelyanov
Quoted 4 lines

This includes (according the the previous description):

* TCP_REPAIR sockoption

Quoted 7 lines
Quoted 5 lines

This one just puts the socket in/out of the repair mode. Allowed for CAP_SYS_ADMIN and for closed/establised sockets only. When repair mode is turned off and the socket happens to be in the established state the window probe is sent to the peer to 'unlock' the connection.

Why not CAP_NET_ADMIN, or something similar?

Agreed, we made this kind of mistake with the transparent proxy stuff, let's not do that again.

#5 lore raw

PATCH 3/3 tcp: Repair socket queues

In reply to Pavel Emelyanov

Reading queues under repair mode is done with recvmsg call. The queue-under-repair set by TCP_REPAIR_QUEUE option is used to determine which queue should be read. Thus both send and receive queue can be read with this.

Caller must pass the MSG_PEEK flag.

Writing to queues is done with sendmsg call and yet again -- the repair-queue option can be used to push data into the receive queue.

When putting an skb into receive queue a zero tcp header is appented to its head to address the tcp_hdr(skb)->syn and the ->fin checks by the (after repair) tcp_recvmsg. (This is not fully correct, yes, need more work with the ->fin one).

When the repair mode is turned off, the write queue seqs are updated so that the whole queue is considered to be 'already sent, waiting for ACKs' (write_seq = snd_nxt <= snd_una). From the protocol POV the send queue looks like it was sent, but the data between the write_seq and snd_nxt is lost in the network.

This helps to avoid another sockoption for setting the snd_nxt sequence. Leaving the whole queue in a 'not yet sent' state (as it will be after sendmsg-s) will not allow to receive any acks from the peer since the ack_seq will be after the snd_nxt. Thus even the ack for the window probe will be dropped and the connection will be 'locked' with the zero peer window.

---
 net/ipv4/tcp.c        |   90 +++++++++++++++++++++++++++++++++++++++++++++++--
 net/ipv4/tcp_output.c |    1 +
 2 files changed, 88 insertions(+), 3 deletions(-)
net/ipv4/tcp.c +86 -3
index 8d9b2bc..eba6696 100644
@@ -909,6 +909,39 @@ static inline int select_size(const struct sock *sk, bool sg)
 	return tmp;
 }
 
+static int tcp_send_rcvq(struct sock *sk, struct msghdr *msg, size_t size)
+{
+	struct sk_buff *skb;
+	struct tcp_skb_cb *cb;
+	struct tcphdr *th;
+
+	skb = alloc_skb(size + sizeof(*th), sk->sk_allocation);
+	if (!skb)
+		goto err;
+
+	th = (struct tcphdr *)skb_put(skb, sizeof(*th));
+	skb_reset_transport_header(skb);
+	memset(th, 0, sizeof(*th));
+
+	if (memcpy_fromiovec(skb_put(skb, size), msg->msg_iov, size))
+		goto err_free;
+
+	cb = TCP_SKB_CB(skb);
+
+	TCP_SKB_CB(skb)->seq = tcp_sk(sk)->rcv_nxt;
+	TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(skb)->seq + size;
+	TCP_SKB_CB(skb)->ack_seq = tcp_sk(sk)->snd_una - 1;
+
+	tcp_queue_rcv(sk, skb, sizeof(*th));
+
+	return size;
+
+err_free:
+	kfree_skb(skb);
+err:
+	return -ENOMEM;
+}
+
 int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
 		size_t size)
 {
@@ -930,6 +964,19 @@ int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
 		if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
 			goto out_err;
 
+	if (unlikely(tp->repair)) {
+		if (tp->repair_queue == TCP_RECV_QUEUE) {
+			copied = tcp_send_rcvq(sk, msg, size);
+			goto out;
+		}
+
+		err = -EINVAL;
+		if (tp->repair_queue == TCP_NO_QUEUE)
+			goto out_err;
+
+		/* 'common' sending to sendq */
+	}
+
 	/* This should be in poll */
 	clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
 
@@ -1087,7 +1134,7 @@ new_segment:
 			if ((seglen -= copy) == 0 && iovlen == 0)
 				goto out;
 
-			if (skb->len < max || (flags & MSG_OOB))
+			if (skb->len < max || (flags & MSG_OOB) || tp->repair)
 				continue;
 
 			if (forced_push(tp)) {
@@ -1100,7 +1147,7 @@ new_segment:
 wait_for_sndbuf:
 			set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
 wait_for_memory:
-			if (copied)
+			if (copied && !tp->repair)
 				tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
 
 			if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
@@ -1111,7 +1158,7 @@ wait_for_memory:
 	}
 
 out:
-	if (copied)
+	if (copied && !tp->repair)
 		tcp_push(sk, flags, mss_now, tp->nonagle);
 	release_sock(sk);
 	return copied;
@@ -1185,6 +1232,24 @@ static int tcp_recv_urg(struct sock *sk, struct msghdr *msg, int len, int flags)
 	return -EAGAIN;
 }
 
+static int tcp_peek_sndq(struct sock *sk, struct msghdr *msg, int len)
+{
+	struct sk_buff *skb;
+	int copied = 0, err = 0;
+
+	/* XXX -- need to support SO_PEEK_OFF */
+
+	skb_queue_walk(&sk->sk_write_queue, skb) {
+		err = skb_copy_datagram_iovec(skb, 0, msg->msg_iov, skb->len);
+		if (err)
+			break;
+
+		copied += skb->len;
+	}
+
+	return err ?: copied;
+}
+
 /* Clean up the receive buffer for full frames taken by the user,
  * then send an ACK if necessary.  COPIED is the number of bytes
  * tcp_recvmsg has given to the user so far, it speeds up the
@@ -1430,6 +1495,21 @@ int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
 	if (flags & MSG_OOB)
 		goto recv_urg;
 
+	if (unlikely(tp->repair)) {
+		err = -EPERM;
+		if (!(flags & MSG_PEEK))
+			goto out;
+
+		if (tp->repair_queue == TCP_SEND_QUEUE)
+			goto recv_sndq;
+
+		err = -EINVAL;
+		if (tp->repair_queue == TCP_NO_QUEUE)
+			goto out;
+
+		/* 'common' recv queue MSG_PEEK-ing */
+	}
+
 	seq = &tp->copied_seq;
 	if (flags & MSG_PEEK) {
 		peek_seq = tp->copied_seq;
@@ -1780,6 +1860,10 @@ out:
 recv_urg:
 	err = tcp_recv_urg(sk, msg, len, flags);
 	goto out;
+
+recv_sndq:
+	err = tcp_peek_sndq(sk, msg, len);
+	goto out;
 }
 EXPORT_SYMBOL(tcp_recvmsg);
 
net/ipv4/tcp_output.c +1 -0
index f0525d1..5e58c1a 100644
@@ -2796,6 +2796,7 @@ void tcp_send_window_probe(struct sock *sk)
 {
 	if (sk->sk_state == TCP_ESTABLISHED) {
 		tcp_sk(sk)->snd_wl1 = tcp_sk(sk)->rcv_nxt - 1;
+		tcp_sk(sk)->snd_nxt = tcp_sk(sk)->write_seq;
 		tcp_xmit_probe_skb(sk, 0);
 	}
 }
Signature · 1 line
1.5.5.6
maintainer
#6 lore raw
In reply to Pavel Emelyanov

From: Pavel Emelyanov <[email protected]>
Date: Tue, 06 Mar 2012 13:54:38 +0400

Quoted 1 line

Attempt #2 with transparent TCP connection hijacking.

This looks a lot better, the TCP_CLOSE state check makes it more clear how this is going to be used.

Please fix up the indentation in patch #2.

+ if (!capable(CAP_SYS_ADMIN) || !(sk->sk_state == TCP_CLOSE ||
+ sk->sk_state == TCP_ESTABLISHED))

And once you sort out the details of the ->{syn,fin} bit in the fake headers added to the repair queues you can resubmit this.

Thanks.