Simple Netcat tool written in Go

It’s spectacularly simple to implement TCP Netcat in Go thanks to io.Copy.

type Progress struct {
	bytes uint64
}

func TransferStreams(con net.Conn) {
	c := make(chan Progress)

	// Read from Reader and write to Writer until EOF
	copy := func(r io.ReadCloser, w io.WriteCloser) {
		defer func() {
			r.Close()
			w.Close()
		}()
		n, _ := io.Copy(w, r)
		c <- Progress{bytes: uint64(n)}
	}

	go copy(con, os.Stdout)
	go copy(os.Stdin, con)

	p := <-c
	log.Printf("[%s]: Connection has been closed by remote peer, %d bytes has been received\n", con.RemoteAddr(), p.bytes)
	p = <-c
	log.Printf("[%s]: Local peer has been stopped, %d bytes has been sent\n", con.RemoteAddr(), p.bytes)
}

But it’s much more harder to do the same for UDP. UDP is connectionless so there is no “streams” that can be copied. You must tediously read data from connection to buffer and write this buffer to stdout by one goroutine. And read data from stdin and write it to connection from other goroutine.

Some remarks:

  • Without streams there is no EOF so we must use some predefined disconnect sequence to terminate transfer.
  • Without established connection a listener doesn’t know the remote peer address until actual data will be received. So listener must wait for data, then read this data and remote address with *net.UDPConn.ReadFrom.

const (
	// BufferLimit specifies buffer size that is sufficient to
	// handle full-size UDP datagram or TCP segment in one step
	BufferLimit = 2<<16 - 1
	// DisconnectSequence is used to disconnect UDP sessions
	DisconnectSequence = "~."
)

// Progress indicates transfer status
type Progress struct {
	remoteAddr net.Addr
	bytes      uint64
}

func TransferPackets(con net.Conn) {
	c := make(chan Progress)

	// Read from Reader and write to Writer until EOF.
	// ra is an address to whom packets must be sent in listen mode.
	copy := func(r io.ReadCloser, w io.WriteCloser, ra net.Addr) {
		defer func() {
			r.Close()
			w.Close()
		}()

		buf := make([]byte, BufferLimit)
		bytes := uint64(0)
		var n int
		var err error
		var addr net.Addr

		for {
			// Read
			if con, ok := r.(*net.UDPConn); ok {
				n, addr, err = con.ReadFrom(buf)
				// In listen mode remote address is unknown until read from
				// connection. Inform caller function with this address.
				if con.RemoteAddr() == nil && ra == nil {
					ra = addr
					c <- Progress{remoteAddr: ra}
				}
			} else {
				n, err = r.Read(buf)
			}
			if err != nil {
				if err != io.EOF {
					log.Printf("[%s]: ERROR: %s\n", ra, err)
				}
				break
			}
			if string(buf[0:n-1]) == DisconnectSequence {
				break
			}

			// Write
			if con, ok := w.(*net.UDPConn); ok && con.RemoteAddr() == nil {
				// Connection remote address must be nil otherwise
				// "WriteTo with pre-connected connection" will be thrown
				n, err = con.WriteTo(buf[0:n], ra)
			} else {
				n, err = w.Write(buf[0:n])
			}
			if err != nil {
				log.Printf("[%s]: ERROR: %s\n", ra, err)
				break
			}
			bytes += uint64(n)
		}
		c <- Progress{bytes: bytes}
	}

	ra := con.RemoteAddr()
	go copy(con, os.Stdout, ra)
	// If connection hasn't got remote address then wait for it from receiver goroutine
	if ra == nil {
		p := <-c
		ra = p.remoteAddr
		log.Printf("[%s]: Datagram has been received\n", ra)
	}
	go copy(os.Stdin, con, ra)

	p := <-c
	log.Printf("[%s]: Connection has been closed, %d bytes has been received\n", ra, p.bytes)
	p = <-c
	log.Printf("[%s]: Local peer has been stopped, %d bytes has been sent\n", ra, p.bytes)
}

Links:

Связывание контейнеров

Это вторая статья цикла Building test environments with Docker.

При создании тестовых окружений из нескольких контейнеров неизбежно возникает задача их взаимного связывания. Набивший оскомину пример: контейнеру с приложением нужен контейнер БД. В нашем же случае, контейнеру с балансером нужны контейнеры с апстримами.

Статья Linking Containers Together полностью раскрывает вопрос линковки контейнеров. Осветим вкратце лишь основные моменты:

  • каждый контейнер необходимо как-то назвать с помощью опции --name;
  • ссылка на контейнер-зависимость обозначается опцией --link;
  • в итоге, внутри зависимого контейнера, инициализируется множество переменных окружения, содержащих параметры контейнера-зависимости, а также в /etc/hosts заносится IP-адрес контейнера-зависимости.

Например, так выглядит последовательный запуск 3-х контейнеров, причем 3-й зависит от первых двух:

docker run -d --privileged -p 2021:22 -p 8081:80 --name app1 smile/tomcat7
docker run -d --privileged -p 2022:22 -p 8082:80 --name app2 smile/tomcat7
docker run -d --privileged -p 2020:22 -p 80:80 --name gate --link app1:app1 --link app2:app2 smile/gate

Для проверки можно использовать вывод docker inspect:

docker inspect -f "{{ .HostConfig.Links }}" gate

[/app1:/gate/app1 /app2:/gate/app2]

Опция -d (detach mode) здесь необходима, чтобы контейнеры запускались в фоновом режиме и не захватывали консоль.

Теперь, если зайти в контейнер gate (ssh -p 2020 root@localhost) и посмотреть переменные окружения, то будет ясно, что gate “видит” exposed-порты и IP-адрес контейнера-зависимости:

root@aba982937531:~# env | grep APP1
APP1_NAME=/gate/app1
APP1_PORT_22_TCP=tcp://172.17.0.28:22
APP1_PORT_80_TCP=tcp://172.17.0.28:80
APP1_PORT_22_TCP_ADDR=172.17.0.28
APP1_PORT_80_TCP_ADDR=172.17.0.28
APP1_PORT_22_TCP_PORT=22
APP1_PORT_80_TCP_PORT=80
APP1_PORT_80_TCP_PROTO=tcp
APP1_PORT_22_TCP_PROTO=tcp
APP1_PORT=tcp://172.17.0.28:22
...

Еще лучше дела обстоят с /etc/hosts:

root@aba982937531:~# grep app1 /etc/hosts
172.17.0.28	app1

Модификация /etc/hosts, например, дает возможность писать следующие кофиги Nginx:

server {
    listen 80 default_server;
    server_name _;

    location /app1 {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #
        proxy_pass http://app1:80/app1;                              # app1 host here
        proxy_redirect http://127.0.0.1:8081/app1 /app1;             #
    }

    location /app2 {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #
        proxy_pass http://app2:80/app2;                              # and app2 host here
        proxy_redirect http://127.0.0.1:8082/app2 /app2;             #
    }
}

Таким образом, в первом приближении, встроенная возможность связывания контейнеров в Docker решает наши проблемы.

Bind interfaces to multiple zones with Firewalld on CentOS-7

As you can expect from man firewall-cmd interface binding to zone other than default (public) could be achieved with the following command sequence:

firewall-cmd --zone public --remove-interface eth1 --permanent
firewall-cmd --zone internal --add-interface eth1 --permanent
firewall-cmd --reload

Seems like it’s done:

firewall-cmd --get-zone-of-interface=eth1
internal

But after firewalld restart or server reboot things aren’t so bright:

firewall-cmd --get-zone-of-interface=eth1
public

The reason is in this CentOS-7 bug. The only workaround is to specify zone in /etc/sysconfig/network-scripts/ifcfg-eth1 file:

TYPE=Ethernet
NAME="eth1"
IPADDR=x.x.x.x
...
ZONE=internal

Docker network and Swarm mode links

Some useful articles & videos about modern networking in Docker:

Swarm mode was presented in Docker 1.12:

Docker registry on Centos 7

1. Create logical volumes for direct-lvm production mode

Assume that we have 40 GByte block device named as /dev/sdb with one full-size Linux partition on it.

Official Device Mapper storage driver guide recommends to use thin pools now. Use these commands to create thin-provisioned logical volumes:

pvcreate /dev/sdb1                 # Create physical volume
vgcreate docker /dev/sdb1          # Create volume group and add this physical volume to it
# Create logical volumes
lvcreate --wipesignatures y -n data docker -l 40%VG
lvcreate --wipesignatures y -n registry docker -l 40%VG
lvcreate --wipesignatures y -n metadata docker -l 2%VG
# Convert data volume to thin pool's data volume
lvconvert -y --zero n -c 512K --thinpool docker/data --poolmetadata docker/metadata
# Set thin pool autoextend features
cat > /etc/lvm/profile/docker-data.profile
activation {
        thin_pool_autoextend_threshold = 80
        thin_pool_autoextend_percent = 20
}
lvchange --metadataprofile docker-data docker/data
# Check thin pool volume (must be monitored) 
lvs -o+seg_monitor
  LV       VG     Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Monitor
  root     centos -wi-ao---- 117,19g
  swap     centos -wi-ao----   1,95g
  data     docker twi-a-t---  16,00g             0,00   0,01                             monitored
  registry docker -wi-a-----  16,00g

Or if you do not trust thin pools use more traditional (but deprecated in Docker) way:

pvcreate /dev/sdb1                 # Create physical volume
vgcreate docker /dev/sdb1          # Create volume group and add this physical volume to it
lvcreate -L 2G -n metadata docker  # Create logical volume for Docker metadata
lvcreate -L 15G -n data docker     # Create logical volume for Docker data (layers, containers etc)
lvcreate -L 15G -n registry docker # Create logical volume for Docker Registry data

Mount volume for Docker registry:

mkfs.xfs /dev/docker/registry
echo "/dev/docker/registry /var/lib/docker-registry    xfs     defaults        1 3" >> /etc/fstab 
mount -a

Check:

lsblk
NAME                             MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda                                8:0    0   120G  0 disk
├─sda1                             8:1    0   876M  0 part /boot
└─sda2                             8:2    0 119,1G  0 part
  ├─centos-swap                  253:0    0     2G  0 lvm  [SWAP]
  └─centos-root                  253:1    0 117,2G  0 lvm  /
sdb                                8:16   0    40G  0 disk
└─sdb1                             8:17   0    40G  0 part
  ├─docker-metadata              253:2    0     2G  0 lvm
  │ └─docker-253:1-23762136-pool 253:5    0    15G  0 dm
  ├─docker-data                  253:3    0    15G  0 lvm
  │ └─docker-253:1-23762136-pool 253:5    0    15G  0 dm
  └─docker-registry              253:4    0    15G  0 lvm  /var/lib/docker-registry

2. Configure Docker daemon

Create systemd drop-in file:

mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/env.conf 
[Service]
EnvironmentFile=-/etc/sysconfig/docker
ExecStart=
ExecStart=/usr/bin/dockerd $OPTIONS $DOCKER_NETWORK_OPTIONS $DOCKER_STORAGE_OPTIONS

Specify Docker configuration:

cat > /etc/sysconfig/docker 
OPTIONS='--iptables=false'
DOCKER_NETWORK_OPTIONS=''
DOCKER_STORAGE_OPTIONS='--storage-driver=devicemapper --storage-opt dm.datadev=/dev/docker/data --storage-opt dm.metadatadev=/dev/docker/metadata'

Check:

systemctl daemon-reload
systemctl show docker | grep EnvironmentFile
EnvironmentFile=/etc/sysconfig/docker (ignore_errors=yes)

And run:

systemctl enable docker
systemctl restart docker

Check again:

docker info | grep data
 Data file: /dev/docker/data
 Metadata file: /dev/docker/metadata
 Metadata Space Used: 639 kB
 Metadata Space Total: 2.147 GB
 Metadata Space Available: 2.147 GB

3. Obtain SSL certificate from Let’s Encrypt

It’s can be done by different ways, see Let’s Encrypt with lego and Nginx for one of these.

Assume that certificate and key was obtained and stored in /etc/pki/tls/lego/certificates directory.

4. Run Docker registry container as systemd unit

Create systemd unit:

cat > /etc/systemd/system/docker-registry.service
[Unit]
Description=Docker registry container
Requires=docker.service
After=docker.service

[Service]
Restart=always
ExecStartPre=/usr/bin/docker create -p 5000:5000 -v /var/lib/docker-registry:/var/lib/registry -v /etc/pki/tls/lego/certificates:/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/example.org.crt -e REGISTRY_HTTP_TLS_KEY=/certs/example.org.key --name registry registry:2
ExecStart=/usr/bin/docker start -a registry
ExecStop=/usr/bin/docker stop -t 5 registry
ExecStopPost=/usr/bin/docker rm registry

[Install]
WantedBy=multi-user.target

5. Permit access to Docker registry only from trusted networks

firewall-cmd --zone=trusted --add-port=5000/tcp --permanent
firewall-cmd --zone=trusted --add-source=192.168.1.0/24 --permanent
firewall-cmd --reload

Since Docker daemon was launched with --iptables=false option, Docker registry port may be accessed from trusted networks only.

Links:

Java network listeners

I’ve written a small listeners library today. It allows to create Callables which can be submitted to ExecutorService. The callable itself implements creating server socket and binding it to local port.

There two principal type of listeners: blocking and non-blocking (thanks to Java NIO.


Blocking listener is very simple but in can’t be interrupted by calling thread. So there’s no point in that:

Future<Socket> future = executor.submit( Listeners.createListener( PORT ) );
try {
    Socket socket = future.get( 1, TimeUnit.SECONDS );
} catch( TimeoutException e ) {
    future.cancel( true );
}

Listener will stay active and PORT will be bound still. The reason is in usage of the uninterruptible ServerSocket.accept().


In contrary non-blocking listener is more sophisticated but it can be interrupted by calling thread. So you can do that:

Future<Socket> future = executor.submit( Listeners.createListener( PORT ) );
try {
    Socket socket = future.get( 1, TimeUnit.SECONDS );
} catch( TimeoutException e ) {
    future.cancel( true );
}

Listener will be terminated and PORT will be freed.

Kafka tips & tricks


Список консьюмер-групп

docker run wurstmeister/kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server kafka:9092 --list

Информация по консьюмер-группе

docker run wurstmeister/kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server kafka:9092 --group id1 --describe

Установка оффсета на начало

docker run wurstmeister/kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server kafka:9092 --topic topic --group id1 --reset-offsets --to-earliest --execute

Установка оффсета на конец

docker run wurstmeister/kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server kafka:9092 --topic topic --group id1 --reset-offsets --to-latest --execute

Установка оффсета на дату-время

docker run wurstmeister/kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server kafka:9092 --topic topic --group id1 --reset-offsets --to-datetime "2017-12-22T00:00:00.000" --execute

Установка оффсета на дату-время для партишенов 0, 1 (одно время для всех партишенов)

docker run wurstmeister/kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server kafka:9092 --topic topic:0,1 --group id1 --reset-offsets --to-datetime "2017-12-22T00:00:00.000" --execute

Установка оффсета на дату-время для партишенов 0, 1 (разное время для партишенов)

docker run dddpaul/kafka-rewind --servers=kafka:9092 --group-id=id1 --topic=topic -o 0=2017-12-01 -o 1=2018-01-01

Создать топик

docker exec -it wurstmeister/kafka sh -c "JMX_PORT=10001 /opt/kafka/bin/kafka-topics.sh --create --topic topic --replication-factor 1 --partitions 1 --zookeeper zookeeper:2181"

Накидать сообщений

docker exec -it wurstmeister/kafka sh -c "JMX_PORT=10001 /opt/kafka/bin/kafka-verifiable-producer.sh --topic topic --max-messages 200000 --broker-list localhost:9092"

Консольный консьюмер

docker exec -it wurstmeister/kafka sh -c "JMX_PORT=10001 /opt/kafka/bin/kafka-console-consumer.sh --topic topic --bootstrap-server host:9092"
docker run -it wurstmeister/kafka -c "JMX_PORT=10001 /opt/kafka/bin/kafka-console-consumer.sh --topic topic --bootstrap-server host:9092"
docker run --entrypoint=/opt/kafka/bin/kafka-console-consumer.sh wurstmeister/kafka --topic topic --bootstrap-server host:9092

Python консьюмер

#!/usr/bin/env python
from kafka import KafkaConsumer
consumer = KafkaConsumer(bootstrap_servers='kafka1:9092',
                         group_id=None,
                         auto_offset_reset='earliest')
consumer.subscribe(['rsyslog_apps'])
for msg in consumer:
    print msg

Kafkacat консьюмер

docker run -it confluentinc/cp-kafkacat kafkacat -b host:9092 -t topic -o beginning -v

Установка retention на топик

docker exec -it kafka /opt/kafka/bin/kafka-configs.sh --zookeeper host:2181 --entity-type topics --entity-name topic --describe
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --zookeeper host:2181 --topic topic --describe
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --zookeeper host:2181 --topic topic --alter --config retention.ms=1000

Let's Encrypt with lego and Nginx

xenolf/lego it’s a feature-rich Let’s Encrypt client and ACME library written in Go.

1. Prepare Nginx server

server {
    listen 80 default;
    server_name example.org www.example.org;

    location /.well-known/acme-challenge {
        proxy_pass http://127.0.0.1:81;
        proxy_set_header Host $host;
    }

    # Other directives
}

2. Update ca-certificates for CentOS 5 (optional)

Let’s Encrypt CA certificate is not included into root CA bundle of old Linux distributions like RHEL/Centos 5. You have to replace this bundle manually with fresh one from cURL website:

cp /etc/pki/tls/certs/ca-bundle.crt /etc/pki/tls/certs/ca-bundle.crt.bak
wget -O /etc/pki/tls/certs/ca-bundle.crt http://curl.haxx.se/ca/cacert.pem

3. Order the certificate from Let’s Encrypt

lego -d example.org -d www.example.org -m cert-owner@example.org -a --path=/etc/pki/tls/lego --http=:81 run

4. Update Nginx server

server {
    listen 80 default;
    server_name example.org www.example.org;

    location /.well-known/acme-challenge {
        proxy_pass http://127.0.0.1:81;
        proxy_set_header Host $host;
    }

    # Other directives
}

server {
    listen 443 ssl;
    server_name example.org www.example.org;

    ssl_certificate /etc/pki/tls/lego/certificates/example.org.crt;
    ssl_certificate_key /etc/pki/tls/lego/certificates/example.org.key;

    location /.well-known/acme-challenge {
        proxy_pass http://127.0.0.1:444;
        proxy_set_header Host $host;
    }

    # Other directives
}

5. Renew certificate every 2 month at 01:30 of first day of the month

Add to crontab:

30 01 01 */2 * /usr/local/bin/lego -d example.org -d www.example.org -m cert-owner@example.org -a --path=/etc/pki/tls/lego --http=:81 --tls=:444 renew && /usr/sbin/nginx -s reload

Links:

Запуск контейнеров с помощью Fig

Это третья статья цикла Building test environments with Docker.

Как мы уже убедились, запуск контейнеров с помощью docker run — занятие весьма муторное, т.к. необходимо указывать множество опций. При запуске же нескольких контейнеров ситуация только ухудшается, т.к. теперь нужно задавать имена и линки.

Эту проблему решает инструмент Fig, который может запустить/остановить целое тестовое окружение, состоящее из набора контейнеров. Описание контейнеров задано в YAML-файле. Таким образом, этот YAML-файл представляет собой конфигурацию тестового окружения.

Конфигурация нашего тестового окружения test-env/fig.yml:

app1:
  image: smile/app1
  ports: ['2021:22', '8081:80']
  privileged: true

app2:
  image: smile/app2
  ports: ['2022:22', '8082:80']
  privileged: true

gate:
  image: smile/gate
  links: [app1, app2]
  ports: ['2020:22', '80:80']
  privileged: true

Запуск тестового окружения (находясь в каталоге с файлом fig.yml): fig up.

По-умолчанию, Fig захватывает консоль и мультиплексирует вывод всех контейнеров. Для запуска в detached mode нужно использовать опцию -d. Посмотреть вывод контейнеров в этом режиме можно с помощью команды fig logs.

Статус тестового окружения: fig ps.

Остановка тестового окружения: fig stop.

Запуск тестового окружения с помощью Upstart

Job /etc/init/fig-test-env.conf создан на основе HeyImAlex/fig.conf. Для отключения автостарта нужно закомментировать вторую строчку (“start on …”).

description "Test environment runner"
start on filesystem and started docker
stop on runlevel [!2345]
respawn
chdir /path/to/test-env
script
  # Wait for docker to finish starting up first.
  FILE=/var/run/docker.sock
  while [ ! -e $FILE ] ; do
    inotifywait -t 2 -e create $(dirname $FILE)
  done
  /usr/local/bin/fig up
end script
post-stop script
 /usr/local/bin/fig stop
end script

Теперь можно запустить тестовое окружение из любого места командой start fig-test-env, а остановить — stop fig-test-env.

Подготовка и запуск docker-контейнеров

Это первая статья цикла Building test environments with Docker.

Сразу оговорюсь, что все docker-контейнеры основаны на baseimage-docker. Этот образ позволяет запускать в контейнере несколько приложений с помощью супервизора runit и содержит ssh, cron, syslog “из коробки”.

Хотя подобный подход не рекомендуется разработчиками Docker, он очень удобен в эксплуатации и не принуждает разработчика к своему “proper way”. Всегда можно использовать канонический подход от Docker с volumes и nsenter, а, при желании, подключаться к контейнерам по ssh.

Кроме того, я привык использовать сервера приложений в связке с nginx, и baseimage-docker позволяет легко это сделать.

Базовый образ

При создании базового образа выполняются две вещи:

  • публичный ssh-ключ добавляется в список разрешенных для суперпользователя контейнера;
  • задается локаль.

Dockerfile базового образа:

FROM phusion/baseimage:0.9.15

# Add public SSH keys
ADD my_rsa_public_key /tmp/my_rsa_public_key
RUN cat /tmp/my_rsa_public_key >> /root/.ssh/authorized_keys && rm -f /tmp/my_rsa_public_key

# Locale
ENV LANG=en_US.utf8

# Use baseimage-docker's init system
CMD ["/sbin/my_init"]

Свой публичный ssh-ключ, естественно, надо положить в my_rsa_public_key рядом с Dockerfile.

Сборка: docker build -t smile/base . Запуск: docker run --rm -it -p 2022:22 smile/base /sbin/my_init -- bash -l

Примечания:

  • --rm — используется для удаления контейнера после его остановки,
  • -it — для терминального интерактивного режима,
  • -- bash -l — для запуска шелла после запуска всех сервисов.

Также на контейнер можно зайти по ssh: ssh -p 2022 root@localhost.

Образ с nginx

Сборка docker-nginx: docker build -t smile/nginx .

Основная особенность этого образа в том, что при запуске контейнера отключается IPv6.

Сделано это для того, чтобы обойти известную проблему с проксированием Nginx. Т.к. демон висит на tcp6, то апстрим иногда видит запрос от 127.0.0.1, а иногда от 0:0:0:0:0:0:0:1. Это принуждает к изменению ACL на сервере приложений, что не всегда удобно.

IPv6 отключается после выполнения серии sysctl-команд, которые требуют, чтобы контейнер был запущен в привилегированном режиме, т.е. команда запуска слегка усложняется:

docker run --privileged --rm -it -p 2022:22 smile/nginx /sbin/my_init -- bash -l

Остальные образы

На основе docker-nginx можно строить более сложные образы, такие как docker-java7-server и docker-tomcat7. На основе последнего уже строятся образы для конечных приложений.