Showing posts with label TCP. Show all posts
Showing posts with label TCP. Show all posts

Thursday, December 26, 2013

Pipelining and flow control

If you are about to create your own application level protocol on top of TCP to load your backend to its limit you should know about how to design such a protocol. Two things that go into mind immediately are pipelining and flow control.
Pipelining is what you have from the box if you are using stream based transport layer. For higher level protocols one needs not to throw it away, for instance, HTTP 1.1 supports pipelining. In brief this is to eliminate latency and jitter between client and server.
To see what flow control is take a look at Akka IO Write models. This gives an ability to the server to say don't push at me, slow down. Indeed TCP too implements ack based flow control.
How to use it? For instance, imagine you have a queue of tasks on server side that is filled by clients and processed by backend. In case clients send tasks too quick the length of the queue grows. One needs to introduce so named high watermark and low watermark. If queue length is greater than high watermark stop reading from sockets and queue length will decrease. When queue length becomes less than low watermark start reading tasks from sockets again.
Note, to make it possible for clients to adapt to speed you process tasks (actually to adapt window size) one shouldn't make a big gap between high and low watermarks. From the other side small gap means you'll be too often add/remove sockets from the event loop.
Some excerpt from real project that uses libev below
//------------------------------------------------------------------

static request_s *request_new(connection_s *con) {
    request_s *new_request;

    new_request = alloc_data(request_mem_mng);
    if (!new_request) {
        log_err("cannot allocate memory");
        goto err;
    }
    memset(new_request, 0, sizeof(request_s));
    
    // Add to connection's list of requests
    list_add_tail(&new_request->request_list, &con->request_list);
    
    new_request->con = con;
    
    {
        // Flow control
        num_reqs++;
        
        if (num_reqs == REQUEST_HIGH_WATERMARK) {
            list_s *elt;
            connection_s *con;
            for (elt = connection_list.next; elt != &connection_list; elt = elt->next) {
                con = list_elt(elt, connection_s, connection_list);
                ev_io_stop(e_loop, &con->read_watcher);
            }
        }
    
    }
    
    return new_request;
err:
    return NULL;
}

//------------------------------------------------------------------

static void request_del(request_s *req) {
    list_del(&req->request_list);
    list_del(&req->request_wait);

    if (req->data)
        free_data(data_mem_mng, req->data);

    free_data(request_mem_mng, req);

    {
        // Flow control
        num_reqs--;
        
        if (num_reqs == REQUEST_LOW_WATERMARK) {
            list_s *elt;
            connection_s *con;
            for (elt = connection_list.next; elt != &connection_list; elt = elt->next) {
                con = list_elt(elt, connection_s, connection_list);
                ev_io_start(e_loop, &con->read_watcher);
            }
        }
    
    }
}
Full source is available.

Tuesday, December 24, 2013

Xen VNC and stale tcp connections

The problem: in case vnc client terminates and doesn't send packet with FIN/RST flag (for instance, VNC connection was tunneled through VPN and tunnel was closed), connection on server side remains in ESTABLISHED state and one cannot connect to this VM via VNC again, see vnc stops working after a while.
The solution of this simple problem is a bit complicated. First of all tcp keepalive on vnc server socket should be turned on, in other words one must patch qemu-dm that implements VNC in Xen (Xen Opensource 3.4.2).
diff -u -x '*.o' -x '*.o.d' xen-3.4.2/tools/ioemu-qemu-xen/osdep.c xen-3.4.2_fix/tools/ioemu-qemu-xen/osdep.c
--- xen-3.4.2/tools/ioemu-qemu-xen/osdep.c      2009-11-05 11:44:56.000000000 +0000
+++ xen-3.4.2_fix/tools/ioemu-qemu-xen/osdep.c  2011-12-28 13:43:29.938649747 +0000
@@ -338,4 +338,11 @@
     f = fcntl(fd, F_GETFL);
     fcntl(fd, F_SETFL, f | O_NONBLOCK);
 }
+
+int socket_set_keepalive(int fd) {
+    int optval;
+    
+    optval = 1;
+    return setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
+}
 #endif
diff -u -x '*.o' -x '*.o.d' xen-3.4.2/tools/ioemu-qemu-xen/qemu_socket.h xen-3.4.2_fix/tools/ioemu-qemu-xen/qemu_socket.h
--- xen-3.4.2/tools/ioemu-qemu-xen/qemu_socket.h        2009-11-05 11:44:56.000000000 +0000
+++ xen-3.4.2_fix/tools/ioemu-qemu-xen/qemu_socket.h    2011-12-28 13:43:32.788255661 +0000
@@ -41,6 +41,7 @@
 
 /* misc helpers */
 void socket_set_nonblock(int fd);
+int socket_set_keepalive(int fd);
 int send_all(int fd, const void *buf, int len1);
 
 /* New, ipv6-ready socket helper functions, see qemu-sockets.c */
diff -u -x '*.o' -x '*.o.d' xen-3.4.2/tools/ioemu-qemu-xen/vnc.c xen-3.4.2_fix/tools/ioemu-qemu-xen/vnc.c
--- xen-3.4.2/tools/ioemu-qemu-xen/vnc.c        2009-11-05 11:44:56.000000000 +0000
+++ xen-3.4.2_fix/tools/ioemu-qemu-xen/vnc.c    2011-12-28 13:44:59.283974757 +0000
@@ -2389,6 +2389,8 @@
        VNC_DEBUG("New client on socket %d\n", vs->csock);
        dcl->idle = 0;
         socket_set_nonblock(vs->csock);
+    if (socket_set_keepalive(vs->csock) == -1)
+        VNC_DEBUG("Cannot set KEEPALIVE on socket %d\n", vs->csock);
        qemu_set_fd_handler2(vs->csock, NULL, vnc_client_read, NULL, opaque);
        vnc_write(vs, "RFB 003.008\n", 12);
        vnc_flush(vs);
Recompile xen tools (make tools, see xen README) and replace qemu-dm executable with dist/install/usr/lib64/xen/bin/qemu-dm.
Configure linux kernel
HDC11:~# sysctl -a | grep ipv4.tcp_keep
net.ipv4.tcp_keepalive_time = 30
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 10
This means in case there are no packets during 30 secs send empty packet, if no ACK received send 5 packets every 10 secs, if still no answer send RST and close connection.
Test
HDC11:~# netstat -nap | grep 5901
tcp 0 0 0.0.0.0:5901 0.0.0.0:* LISTEN 28905/qemu-dm
HDC11:~# netstat -nap | grep 5901
tcp 0 0 0.0.0.0:5901 0.0.0.0:* LISTEN 28905/qemu-dm
tcp 0 0 10.10.17.11:5901 10.10.17.216:1334 ESTABLISHED 28905/qemu-dm <---- VNC connection via VPN
HDC11:~# tcpdump -n -i eth2 port 1334
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth2, link-type EN10MB (Ethernet), capture size 96 bytes
07:42:15.119612 IP 10.10.17.11.5901 > 10.10.17.216.1334: . ack 1601431976 win 5840
07:42:15.316704 IP 10.10.17.216.1334 > 10.10.17.11.5901: . ack 1 win 64228
07:42:45.319980 IP 10.10.17.11.5901 > 10.10.17.216.1334: . ack 1 win 5840 <--- acks every 30 secs
07:42:45.701610 IP 10.10.17.216.1334 > 10.10.17.11.5901: . ack 1 win 64228 <--- response to ack
07:43:15.700307 IP 10.10.17.11.5901 > 10.10.17.216.1334: . ack 1 win 5840 <--- kill vpn, no response anymore
07:43:25.700398 IP 10.10.17.11.5901 > 10.10.17.216.1334: . ack 1 win 5840
07:43:35.700586 IP 10.10.17.11.5901 > 10.10.17.216.1334: . ack 1 win 5840
07:43:45.700639 IP 10.10.17.11.5901 > 10.10.17.216.1334: . ack 1 win 5840
07:43:55.700765 IP 10.10.17.11.5901 > 10.10.17.216.1334: . ack 1 win 5840 <--- 5 probes every 10 secs
07:44:05.700913 IP 10.10.17.11.5901 > 10.10.17.216.1334: R 1:1(0) ack 1 win 5840 <--- still no response, RST
^C
10 packets captured
10 packets received by filter
0 packets dropped by kernel
HDC11:~# netstat -nap | grep 5901
tcp 0 0 0.0.0.0:5901 0.0.0.0:* LISTEN 28905/qemu-dm
<--- no ESTABLISHED connections
HDC11:~# telnet localhost 5901 <--- we can connect to this port again
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
RFB 003.008 <--- server responds
^]
telnet> quit
But we need to go deeper. Tcp keepalive works only when there are no packets on the wire. Imagine VNC server sent some packet and connection terminated in silent way (i.e. it didn't receive ACK in his packet). In this case linux will try to resend packet until RTO.
07:57:36.123013 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1557:1598(41) ack 377 win 5840
07:57:36.317922 IP 10.10.17.216.1421 > 10.10.17.11.5901: P 377:387(10) ack 1598 win 63353 <--- here we lost connection
07:57:36.360862 IP 10.10.17.11.5901 > 10.10.17.216.1421: . ack 387 win 5840 07:58:06.317741 IP 10.10.17.11.5901 > 10.10.17.216.1421: . ack 387 win 5840
07:58:16.321445 IP 10.10.17.11.5901 > 10.10.17.216.1421: . ack 387 win 5840
07:58:16.932260 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840 <--- vnc server (qemu-dm) tries to send some data
07:58:17.621451 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840 <--- retransmit algorithm starts with dynamic growing intervals
07:58:19.001467 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
07:58:21.761500 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
07:58:27.281590 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
07:58:38.321648 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
07:59:00.401919 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
07:59:44.562552 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:01:12.883584 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:03:12.885069 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:05:12.886487 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:07:12.887861 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:09:12.889364 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:11:12.890812 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:13:12.892282 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
08:15:12.893702 IP 10.10.17.11.5901 > 10.10.17.216.1421: P 1598:1617(19) ack 387 win 5840
<--- ok, now reno gives up, connection is closed after ~15 mins
HDC11:~# netstat -nap | grep 5901
tcp 0 0 0.0.0.0:5901 0.0.0.0:* LISTEN 28905/qemu-dm
15 mins without ability to connect to VM is too long for client. Configure kernel again - decrease /proc/sys/net/ipv4/tcp_retries2 (net.ipv4.tcp_retries2) from 15 to 5 tries.
09:05:45.286580 IP 10.10.17.216.1312 > 10.10.17.11.5901: P 84:94(10) ack 3649 win 63679
09:05:45.320669 IP 10.10.17.11.5901 > 10.10.17.216.1312: . ack 94 win 5840
09:06:15.281030 IP 10.10.17.11.5901 > 10.10.17.216.1312: . ack 94 win 5840
09:06:25.281213 IP 10.10.17.11.5901 > 10.10.17.216.1312: . ack 94 win 5840
09:06:25.961866 IP 10.10.17.11.5901 > 10.10.17.216.1312: P 3649:3668(19) ack 94 win 5840
09:06:26.661261 IP 10.10.17.11.5901 > 10.10.17.216.1312: P 3649:3668(19) ack 94 win 5840
09:06:28.061265 IP 10.10.17.11.5901 > 10.10.17.216.1312: P 3649:3668(19) ack 94 win 5840
09:06:30.861301 IP 10.10.17.11.5901 > 10.10.17.216.1312: P 3649:3668(19) ack 94 win 5840
09:06:36.461346 IP 10.10.17.11.5901 > 10.10.17.216.1312: P 3649:3668(19) ack 94 win 5840
09:06:47.661424 IP 10.10.17.11.5901 > 10.10.17.216.1312: P 3649:3668(19) ack 94 win 5840
~60-90 secs
As a result one can reestablish connection to VNC in 1-2 mins.

Note, one can kill stale connection with the help of netfilter by setting conntrack net.netfilter.nf_conntrack_tcp_timeout_established to 1-2 hours (default is 5 days). After this time elapses iptables will send RST in both directions. But one still need to set proper net.ipv4.tcp_retries2.

Monday, December 23, 2013

How to limit network bandwidth and introduce latency

So you want to test clients of some service like nfs in case network or service is slow. In order to do this you need to limit network throughput, introduce latency and jitter (latency variation). Ok, I bet you know how to do this
tc qdisc add dev eth0 root handle 1: prio
tc qdisc add dev eth0 parent 1:3 handle 30: tbf rate 1mbit buffer 10kb limit 3000
tc qdisc add dev eth0 parent 30:1 handle 31: netem delay 100ms 10ms distribution normal
tc filter add dev eth0 protocol ip parent 1:0 prio 3 u32 match ip dst 192.168.44.4/32 flowid 1:3
See Linux Advanced Routing & Traffic Control HOWTO.
Simple test. On server side
iperf -s
------------------------------------------------------------
Server listening on TCP port 5001
TCP window size: 85.3 KByte (default)
------------------------------------------------------------
[  4] local 192.168.44.4 port 5001 connected with 192.168.44.26 port 37494
[ ID] Interval       Transfer     Bandwidth
[  4]  0.0-14.6 sec  1.52 MBytes    870 Kbits/sec
On client side
iperf -c 192.168.44.4
------------------------------------------------------------
Client connecting to 192.168.44.4, TCP port 5001
TCP window size: 16.0 KByte (default)
------------------------------------------------------------
[  3] local 192.168.44.26 port 37494 connected with 192.168.44.4 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-11.8 sec  1.52 MBytes  1.08 Mbits/sec
Cleanup
tc qdisc del dev eth0 root

Friday, December 20, 2013

Programming TUN/TAP in Linux, changing IP address on the fly

So you want to map one network to another by implementing your very own bridge that changes network in IP packets it receives and forwards them to other segment. Why not I say, could be.
The plan is
  1. Create 2 tap interfaces: tap1 and tap2.
  2. Bridge tap1 with eth0 (i.e. everything that reaches tap1 is being forwarded to eth0 by kernel and vice versa).
  3. Application gets packets from tap2, changes src and dst IP addresses (changes network 10.0.0.0/24 to 192.168.14.0/24) and writes resulting packet to tap1 (then to eth0 and to the wires).
  4. When application gets response from eth0 (tap1) it substitutes IPs again and writes to tap2.
Note: the problem with IP substitution is in checksums. IP packets have IP header checksum, TCP and UDP also has checksum of pseudoheader that contains src and dst IP addresses as well. See details in the code.
For more information on TUN/TAP try to read Universal TUN/TAP device driver Frequently Asked Question.
In this way if you are in 192.168.14.0/24 network and want to speak with 192.168.14.15 after small manipulations you can speak with that host as if it had address 10.0.0.15.
The instructions and program itself without any further comments are below.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <stdint.h>
#include <arpa/inet.h>

#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>


/* 
Replace ip addresses in packets on L2 level.

Setup:

tunctl -t tap1
tunctl -t tap2
brctl addif br1 tap1
brctl addif br1 eth0

ifconfig tap1 promisc up
ifconfig tap2 promisc up

ifconfig tap2 10.0.1.18 netmask 255.255.255.0

Send packets to 10.0.0.0/24 on tap2, they will appear as 192.168.14.0/24 on eth0.
Note that these values are harcoded in main.

*/


static int
tun_alloc_old(char *dev) {
    char tunname[IFNAMSIZ];

    sprintf(tunname, "/dev/%s", dev);
    return open(tunname, O_RDWR);
}


static int
tun_alloc(char *dev) {
    struct ifreq    ifr;
    int     fd;
    int     err;

    if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
        return tun_alloc_old(dev);

    memset(&ifr, 0, sizeof(ifr));

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *
     *        IFF_NO_PI - Do not provide packet information
     */
    ifr.ifr_flags = IFF_TAP;
    if (*dev)
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);

    if ((err = ioctl(fd, TUNSETIFF, (void*)&ifr)) < 0) {
        close(fd);
        perror("TUNSETIFF");
        return err;
    }

    strcpy(dev, ifr.ifr_name);
    return fd;
}


static size_t
write2(int fildes, const void *buf, size_t nbyte) {
    int     ret;
    size_t  n;

    n = nbyte;
    while (n > 0) {
        ret = write(fildes, buf, nbyte);
        if (ret < 0)
            return ret;

        n -= ret;
        buf += ret;
    }

    return nbyte;
}


static uint16_t
ipcheck(uint16_t *ptr, size_t len) {
    uint32_t    sum;
    uint16_t    answer;

    sum = 0;

    while (len > 1) {
        sum += *ptr++;
        len -= 2;
    }

    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    answer = ~sum;
    
    return answer;
}


static uint16_t
check2(struct iovec *iov, int iovcnt) {
    long    sum;
    uint16_t    answer;
    struct iovec   *iovp;

    sum = 0;

    for (iovp = iov; iovp < iov + iovcnt; iovp++) {
        uint16_t *ptr;
        size_t len;

        ptr = iovp->iov_base;
        len = iovp->iov_len;

        while (len > 1) {
            sum += *ptr++;
            len -= 2;
        }

        if (len == 1) {
            u_char t[2];
            t[0] = (u_char)*ptr;
            t[1] = 0;

            sum += (uint16_t)*t;
        }

    }

    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    answer = ~sum;
    
    return answer;
}


static void
tcpcheck(struct iphdr *iph, struct tcphdr *tcph, size_t len) {
    struct iovec iov[5];

    iov[0].iov_base = &iph->saddr;
    iov[0].iov_len = 4;
    iov[1].iov_base = &iph->daddr;
    iov[1].iov_len = 4;

    u_char  t[2];
    t[0] = 0;
    t[1] = iph->protocol;
    iov[2].iov_base = t;
    iov[2].iov_len = 2;

    uint16_t l;
    l = htons(tcph->doff * 4 + len);
    iov[3].iov_base = &l;
    iov[3].iov_len = 2;

    iov[4].iov_base = tcph;
    iov[4].iov_len = tcph->doff * 4 + len;

    tcph->check = 0;
    tcph->check = check2(iov, sizeof(iov) / sizeof(struct iovec));
}


static void
udpcheck(struct iphdr *iph, struct udphdr *udph) {
    struct iovec iov[5];

    iov[0].iov_base = &iph->saddr;
    iov[0].iov_len = 4;
    iov[1].iov_base = &iph->daddr;
    iov[1].iov_len = 4;

    u_char  t[2];
    t[0] = 0;
    t[1] = iph->protocol;
    iov[2].iov_base = t;
    iov[2].iov_len = 2;

    uint16_t l;
    l = udph->len;
    iov[3].iov_base = &l;
    iov[3].iov_len = 2;

    iov[4].iov_base = udph;
    iov[4].iov_len = ntohs(udph->len);

    udph->check = 0;
    udph->check = check2(iov, sizeof(iov) / sizeof(struct iovec));
}


static int
substitute(u_char* buf, ssize_t n, u_char* net1, u_char* net2) {

    if (buf[12] == 8 && buf[13] == 6) {
        u_char     *arp;

        arp = buf + 14;

        /* replace ip */
        if (!memcmp(arp + 14, net1, 3)) {
            memcpy(arp + 14, net2, 3);
        }

        if (!memcmp(arp + 24, net1, 3)) {
            memcpy(arp + 24, net2, 3);
        }
    }
    else if (buf[12] == 8 && buf[13] == 0) {
        struct iphdr   *iph;
        size_t      len;


        iph = (struct iphdr*)(buf + 14);
        len = iph->ihl * 4;

        /* clear crc */
        iph->check = 0;


        /* replcace ip */
        if (!memcmp(&iph->saddr, net1, 3)) {
            memcpy(&iph->saddr, net2, 3);
        }

        if (!memcmp(&iph->daddr, net1, 3)) {
            memcpy(&iph->daddr, net2, 3);
        }

        /* put new crc */
        iph->check = ipcheck((uint16_t*)iph, len);


        if (iph->protocol == 6) {
            struct tcphdr  *tcph;

            tcph = (struct tcphdr*)((u_char*)iph + len);
            tcpcheck(iph, tcph, n - ((u_char*)tcph - buf) - tcph->doff * 4);
        }
        else if (iph->protocol == 17) {
            struct udphdr  *udph;

            udph = (struct udphdr*)((u_char*)iph + len);
            udpcheck(iph, udph);
        }
    }

    return 0;
}


int
main(int argc, char **argv) {
    int     tap1;
    int     tap2;
    int     maxfd;
    char    tunname[IFNAMSIZ];
    u_char  buf[15000];
    ssize_t n;

    u_char net1[] = {192, 168, 14};
    u_char net2[] = {10, 0, 1};

    strcpy(tunname, "tap1");
    if ((tap1 = tun_alloc(tunname)) < 0) {
        goto error;
    }

    strcpy(tunname, "tap2");
    if ((tap2 = tun_alloc(tunname)) < 0) {
        goto error;
    }

    maxfd = (tap1 > tap2)? tap1 : tap2;

    while (1) {
        int     ret;
        fd_set  rd_set;
        
        FD_ZERO(&rd_set);
        FD_SET(tap1, &rd_set);
        FD_SET(tap2, &rd_set);
    
        ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
        
        if (ret < 0 && errno == EINTR) {
            continue;
        }

        if (ret < 0) {
            perror("select()");
            goto error;
        }

        if (FD_ISSET(tap1, &rd_set)) {
            n = read(tap1, buf, sizeof(buf));
            if (n < 0)
                goto error;

            if (substitute(buf, n, net1, net2))
                goto error;

            if (write2(tap2, buf, n) < 0)
                goto error;
        }

        if (FD_ISSET(tap2, &rd_set)) {

            n = read(tap2, buf, sizeof(buf));
            if (n < 0)
                goto error;

            if (substitute(buf, n, net2, net1))
                goto error;

            if (write2(tap1, buf, n) < 0)
                goto error;
        }
    }
    
    close(tap1);
    close(tap2);

    return 0;

error:
    return 1;
}