2015年5月28日 星期四

Docker Practice - Network

Docker 中的網路功能介紹#

(P50) Docker 允許透過外部存取容器或容器互聯的方式來提供網路服務。

外部存取容器#

容器中可以執行一些網路應用,要讓外部也可以存取這些應用,可以通過 -P 或 -p 參數來指定連接埠映射。

當使用 -P 參數時,Docker 會隨機映射一個 49000~49900 的連接埠到內部容器開放的網路連接埠。
// -P, --publish-all=false Publish all exposed ports to the host interfaces 
# docker run -itd -P training/webapp python app.py 
# docker ps -l 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
bddf92846861 training/webapp:latest python app.py 5 minutes ago Up 5 minutes 0.0.0.0:49153->5000/tcp dreamy_torvalds 
//

使用 docker ps 可以看到,本地主機的 49153 被映射到了容器的 5000 連接埠。此時連結本機的 49153 連接埠即可連結容器內 web 應用提供的界面。

同樣的,可以透過 docker logs 命令來查看應用的訊息:
Usage: docker logs [OPTIONS] CONTAINER
Fetch the logs of a container

  -f, --follow=false        Follow log output
  -t, --timestamps=false    Show timestamps
  --tail="all"              Number of lines to show from the end of the logs

// -f, --follow=false Follow log output 
# docker logs -f dreamy_torvalds 
* Running on http://0.0.0.0:5000/

-p小寫的)則可以指定要映射的連接埠,並且在一個指定連接埠上只可以綁定一個容器。支援的格式有:
  -p, --publish=[]   Publish a container's port to the host
                     format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort
                     (use 'docker port' to see the actual mapping)

映射所有遠端位址#

使用 hostPort:containerPort 格式可使本地的 5000 連接埠映射到容器的 5000 連接埠,範例如下:
# docker run -d -p 5000:5000 training/webapp python app.py

此時預設會綁定本地所有遠端上的所有 IP 位址.

映射到指定位址的指定連接埠#

可以使用 ip:hostPort:containerPort 格式指定映射使用一個特定位址,比如 localhost 位址 127.0.0.1:
# docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

映射到指定位址的任意連接埠#

使用 ip::containerPort 綁定 localhost 的任意連接埠到容器的 5000 連接埠,本地主機會自動分配一個連接埠:
# docker run -d -p 127.0.0.1::5000 training/webapp python app.py

還可以使用 udp 標記來指定 udp 連接埠:
# docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

查看映射連接埠配置#

使用 docker port 來查看當前映射的連接埠配置,也可以查看到綁定的位址:
Usage: docker port CONTAINER [PRIVATE_PORT[/PROTO]]
List port mappings for the CONTAINER, or lookup the public-facing port that is
NAT-ed to the PRIVATE_PORT

# docker port dreamy_torvalds 5000 
0.0.0.0:49153

注意:
  • 容器有自己的內部網路和 ip 位址(使用 docker inspect 可以獲取所有的變數,Docker 還可以有一個可變的網路設定。
  • -p 標記可以多次使用來綁定多個連接埠.
一個綁定多個範例如下:
# docker run -d -p 5000:5000 -p 3000:80 training/webapp python app.py

容器互聯#

容器的連接(linking)系統是除了連接埠映射外,另一種跟容器中應用互動的方式。

該系統會在來源端容器和接收端容器之間創建一個隧道,接收端容器可以看到來源端容器指定的信息。

自定義容器命名#

連接系統依據容器的名稱來執行。因此,首先需要自定義一個好記的容器命名。

雖然當創建容器的時候,系統會預設分配一個名字。自定義命名容器有2個好處:
  • 自定義的命名,比較好記,比如一個web應用容器我們可以給它起名叫web
  • 當要連接其他容器時候,可以作為一個有用的參考點,比如連接web容器到db容器

使用 --name 標記可以為容器自定義命名:
# docker run -d -P --name web training/webapp python app.py
# docker ps // 驗證設定的命名 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
2cef5d62c704 training/webapp:latest python app.py 6 seconds ago Up 6 seconds 0.0.0.0:49153->5000/tcp web

也可以使用 docker inspect 來查看容器的名字:
// -f, --format="" Format the output using the given go template. 
# docker inspect -f " .Name " 2cef5d62c704 
/web

注意:容器的名稱是唯一的。如果已經命名了一個叫 web 的容器,當你要再次使用 web 這個名稱的時候,需要先用 docker rm 來刪除之前建立的同名容器。

在執行 docker run 的時候如果新增 --rm 標記,則容器在終止後會立刻刪除。注意, --rm 和 -d 參數不能同時使用。

容器互聯#

使用 --link 參數可以讓容器之間安全的進行互動。下面先建立一個新的資料庫容器:
# docker run -dti --name db training/postgres /bin/bash 
5996905ecb1d6dcf87749a02d469c7c7a65a2dbc2abff7668f1665c24291f409

刪除之前建立的 web 容器:
# docker rm -f web 
web

然後建立一個新的 web 容器,並將它連接到 db 容器:
// --link= Add link to another container (name:alias) 
# docker run -dti -P --name web --link db:db training/webapp python app.py 
33ae0b6fb3fa96dc520d5ddfda08b93a506b6e305ba179f423813827f54285be 
# docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
33ae0b6fb3fa training/webapp:latest python app.py 2 minutes ago Up 2 minutes 0.0.0.0:49153->5000/tcp web 
5996905ecb1d training/postgres:latest /bin/bash 3 minutes ago Up 3 minutes 5432/tcp db,web/db

此時,db 容器和 web 容器建立互聯關系。--link 參數的格式為 --link name:alias ,其中 name 是要連接的容器名稱, alias 是這個連接的別名。從上面的輸出可以看到自定義命名的容器,db 和 webdb 容器的 names 列有 db 也有 web/db。這表示 web 容器連接 到 db 容器,web 容器將被允許存取 db 容器的訊息。

Docker 在兩個互聯的容器之間創建了一個安全隧道,而且不用映射它們的連接埠到宿主主機上。在啟動 db 容器的時候並沒有使用 -p 和 -P 標記,從而避免了暴露資料庫連接埠到外部網路上。

Docker 透過 2 種方式為容器公開連接訊息:
  • 環境變數
  • 更新 /etc/hosts 檔案
使用 env 命令來查看 web 容器的環境變數:
# docker run --rm --name web2 --link db:db training/webapp env 
HOME=/ 
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 
HOSTNAME=b73264a55589 
DB_PORT=tcp://172.17.0.44:5432 
DB_PORT_5432_TCP=tcp://172.17.0.44:5432 
DB_PORT_5432_TCP_ADDR=172.17.0.44 
DB_PORT_5432_TCP_PORT=5432 
DB_PORT_5432_TCP_PROTO=tcp 
DB_NAME=/web2/db 
DB_ENV_PG_VERSION=9.3

其中 DB_ 開頭的環境變數是供 web 容器連接 db 容器使用,前綴採用大寫的連接別名。

除了環境變量,Docker 還新增 host 訊息到父容器的 /etc/hosts 的檔案。下面是父容器 web 的 hosts 檔案:
# docker run -ti --rm --link db:db training/webapp /bin/bash 
root@9d4ba1b4ea1e:/opt/webapp# cat /etc/hosts // Inside container 
172.17.0.47 9d4ba1b4ea1e 
... 
172.17.0.44 db

第一個是 web 容器,web 容器用 id 作為他的主機名,第二個是 db 容器的 ip 和主機名。可以在 web 容器中安裝 ping 命令來測試跟 db 容器的連通:
# apt-get install -yqq inetutils-ping 
# ping db 
PING db (172.17.0.44): 48 data bytes 
56 bytes from 172.17.0.44: icmp_seq=0 ttl=64 time=0.342 ms 
56 bytes from 172.17.0.44: icmp_seq=1 ttl=64 time=0.150 ms 
...

用 ping 來測試db容器,它會解析成 172.17.0.44。 *注意:官方的 ubuntu 映像檔預設沒有安裝 ping,需要自行安裝。

使用者可以連接多個子容器到父容器,比如可以連接多個 web 到 db 容器上。

進階網路設定#

(P56) 本章將介紹 Docker 的一些進階網路設定和選項。

當 Docker 啟動時,會自動在主機上建立一個 docker0 虛擬橋接器,實際上是 Linux 的一個 bridge,可以理解為一個軟體交換機。它會在掛載到它的網卡之間進行轉發。同時,Docker 隨機分配一個本地未占用的私有網段(在 RFC1918 中定義)中的一個位址給 docker0 界面。比如典型的 172.17.42.1 ,網路遮罩為 255.255.0.0 。此後啟動的容器內的網卡也會自動分配一個同一網段(172.17.0.0/16)的網址。


當建立一個 Docker 容器的時候,同時會建立了一對 veth pair 界面(當資料包發送到一個界面時,另外一個界面也可以收到相同的資料包)。這對界面一端在容器內,即 eth0 ;另一端在本地並被掛載到 docker0 橋接器,名稱以 veth 開頭(例如 vethAQI2QT)。透過這種方式,主機可以跟容器通信,容器之間也可以相互通信。Docker 就建立了在主機和所有容器之間一個虛擬共享網路。


接下來的部分將介紹在一些場景中,Docker 所有的網路自訂設定。以及透過 Linux 命令來調整、補充、甚至替換 Docker 預設的網路設定。

快速設定指南#

下面是一個跟 Docker 網路相關的命令列表。其中有些命令選項只有在 Docker 服務啟動的時候才能設定,而且不能馬上生效:
  • -b BRIDGE or --bridge=BRIDGE : 指定容器掛載的橋接器
  • --bip=CIDR : 定制 docker0 的遮罩
  • -H SOCKET... or --host=SOCKET... : Docker 服務端接收命令的通道
  • --icc=true|false : 是否支援容器之間進行通信
  • --ip-forward=true|false : 請看下文容器之間的通信
  • --iptables=true|false : 禁止 Docker 新增 iptables 規則
  • --mtu=BYTES : 容器網路中的 MTU

下面2個命令選項既可以在啟動服務時指定,也可以 Docker 容器啟動(docker run)時候指定。在 Docker 服務啟動的時候指定則會成為預設值,後面執行docker run 時可以覆蓋設定的預設值:
  • --dns=IP_ADDRESS... : 使用指定的DNS伺服器
  • --dns-search=DOMAIN... : 指定DNS搜索域

最後這些選項只有在 _docker run 執行時使用,因為它是針對容器的特性內容:
  • -h HOSTNAME or --hostname=HOSTNAME : 設定容器主機名
  • --link=CONTAINER_NAME:ALIAS : 新增到另一個容器的連接
  • --net=bridge|none|container:NAME_or_ID|host : 設定容器的橋接模式
  • -p SPEC or --publish=SPEC : 映射容器連接埠到宿主主機
  • -P or --publish-all=true|false : 映射容器所有連接埠到宿主主機

設定 DNS#

Docker 沒有為每個容器專門定制映像檔,那麽怎麽自定義設定容器的主機名和 DNS 設定呢? 秘訣就是它利用虛擬檔案來掛載到來容器的 3 個相關設定檔案。在容器中使用 mount 命令可以看到掛載訊息:
root@0f320ab6d701:/opt/webapp# mount 
... /dev/disk/by-uuid/23c25e42-ea2c-4b8e-8632-353bfb5cbe10 on /.dockerinit type ext4 (ro,relatime,errors=remount-ro,data=ordered) 
/dev/disk/by-uuid/23c25e42-ea2c-4b8e-8632-353bfb5cbe10 on /etc/resolv.conf type ext4 
(ro,relatime,errors=remount-ro,data=ordered) /dev/disk/by-uuid/23c25e42-ea2c-4b8e-8632-353bfb5cbe10 on /etc/hostname type ext4 
(ro,relatime,errors=remount-ro,data=ordered) /dev/disk/by-uuid/23c25e42-ea2c-4b8e-8632-353bfb5cbe10 on /etc/hosts type ext4 (ro,relatime,errors=remount-ro,data=ordered) 
...

這種機制可以讓宿主主機 DNS 訊息發生更新後,所有 Docker 容器的 dns 設定透過 /etc/resolv.conf 檔案立刻得到更新。

如果使用者想要手動指定容器的設定,可以利用下面的選項:
-h HOSTNAME or --hostname=HOSTNAME 設定容器的主機名,它會被寫到容器內的 /etc/hostname 和 /etc/hosts 。但它在容器外部看不到,既不會在docker ps 中顯示,也不會在其他的容器的 /etc/hosts 看到。

--link=CONTAINER_NAME:ALIAS 選項會在建立容器的時候,新增一個其他容器的主機名到 /etc/hosts 檔案中,讓新容器的程式可以使用主機名 ALIAS 就可以連接它。

--dns=IP_ADDRESS 新增 DNS 伺服器到容器的 /etc/resolv.conf 中,讓容器用這個伺服器來解析所有不在 /etc/hosts 中的主機名。

--dns-search=DOMAIN 設定容器的搜索域,當設定搜索域為 .example.com 時,在搜索一個名為 host 的主機時,DNS 不僅搜索 host,還會搜索host.example.com 。 注意:如果沒有上述最後 2 個選項,Docker 會預設用主機上的 /etc/resolv.conf 來設定容器。

容器存取控制#

(P59) 容器的存取控制,主要透過 Linux 上的 iptables 防火墻來進行管理和實作。 

容器存取外部網路#

容器要想存取外部網路,需要本地系統的轉發支援。在Linux 系統中,可以使用命令 sysctl 檢查轉發是否打開:
# sysctl net.ipv4.ip_forward 
net.ipv4.ip_forward = 1

如果為 0,說明沒有開啟轉發,則需要手動打開:
// -w: Use this option when you want to change a sysctl setting. 
# sysctl -w net.ipv4.ip_forward=1

如果在啟動 Docker 服務的時候設定 --ip-forward=true , Docker 就會自動設定系統的 ip_forward 參數為 1.

容器之間存取#

容器之間相互存取,需要兩方面的支援:
  • 容器的網路拓撲是否已經互聯。預設情況下,所有容器都會被連接到 docker0 橋接器上。
  • 本地系統的防火墻軟件 -- iptables 是否允許透過。

存取所有連接埠
當啟動 Docker 服務時候,預設會新增一條轉發策略到 iptables 的 FORWARD 鏈上。策略為透過(ACCEPT)還是禁止(DROP)取決於設定 --icc=true (預設值)還是 --icc=false 。當然,如果手動指定 --iptables=false 則不會新增 iptables 規則。

可見,預設情況下,不同容器之間是允許網路互通的。如果為了安全考慮,可以在 /etc/default/docker 檔案中設定 DOCKER_OPTS=--icc=false 來禁止它。

存取指定連接埠
在透過 -icc=false 關閉網路存取後,還可以透過 --link=CONTAINER_NAME:ALIAS 選項來存取容器的開放連接埠。例如在啟動 Docker 服務時,可以同時使用icc=false --iptables=true 參數來關閉允許該容器存取其他容器,並讓 Docker 可以修改系統中的 iptables 規則。

一個範例說明如下:
# docker ps // 啟動 Docker 時使用預設參數 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
4c1ba881a91f training/webapp:latest "python app.py" 14 minutes ago Up 14 minutes 0.0.0.0:32768->5000/tcp web 
9fb502b72629 training/postgres:latest "/bin/bash" 14 minutes ago Up 14 minutes 5432/tcp db 


# docker exec web ip address show | grep inet | grep eth0 // 檢視 container web 的 ip 
inet 172.17.0.2/16 scope global eth0 
# docker exec db ip address show | grep inet | grep eth0 // 檢視 Container db 的 ip 
inet 172.17.0.1/16 scope global eth0 

# iptables -nL // Connection between Host and Containers is ok 
...
Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            172.17.0.2           tcp dpt:5000
ACCEPT     tcp  --  172.17.0.2           172.17.0.1           tcp dpt:5432
ACCEPT     tcp  --  172.17.0.1           172.17.0.2           tcp spt:5432
...

映射容器連接埠到宿主主機的實作#

預設情況下,容器可以主動存取到外部網路的連接,但是外部網路無法存取到容器。

容器存取外部實作#

容器所有到外部網路的連接,源位址都會被NAT成本地系統的IP位址。這是使用 iptables 的源位址偽裝操作實作的。查看主機的 NAT 規則:
# iptables -t nat -nL 
... 
Chain POSTROUTING (policy ACCEPT) 
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0 
...

其中,上述規則將所有源位址在 172.17.0.0/16 網段,目標位址為其他網段(外部網路)的流量動態偽裝為從系統網卡發出。MASQUERADE 跟傳統 SNAT 的好處是它能動態從網卡取得位址。

外部存取容器實作#

容器允許外部存取,可以在 docker run 時候透過 -p 或 -P 參數來啟用。

不管用那種辦法,其實也是在本地的 iptable 的 nat 表中新增相應的規則。使用 -P 時:
# iptables -t nat -nL 
... 
Chain DOCKER (2 references) 
target prot opt source destination 
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49153 to:172.17.0.2:80

使用 -p 80:80 時:
# iptables -t nat -nL 
Chain DOCKER (2 references) 
target prot opt source destination 
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80

注意:
這裡的規則映射了 0.0.0.0,意味著將接受主機來自所有界面的流量。使用者可以透過 -p IP:host_port:container_port 或 -p IP::port 來指定允許存取容器的主機上的 IP、界面等,以制定更嚴格的規則。如果希望永久綁定到某個固定的 IP 位址,可以在 Docker 設定檔案 /etc/default/docker 中指定DOCKER_OPTS="--ip=IP_ADDRESS" ,之後重啟 Docker 服務即可生效。

設定 docker0 橋接器#

Docker 服務預設會建立一個 docker0 橋接器(其上有一個 docker0 內部界面),它在核心層連通了其他的物理或虛擬網卡,這就將所有容器和本地主機都放到同一個物理網路。

Docker 預設指定了 docker0 界面 的 IP 位址和子網遮罩,讓主機和容器之間可以透過橋接器相互通信,它還給出了 MTU界面允許接收的最大傳輸單元),通常是 1500 Bytes,或宿主主機網路路由上支援的預設值。這些值都可以在服務啟動的時候進行設定。
  • --bip=CIDR: IP 位址加遮罩格式,例如 192.168.1.5/24
  • --mtu=BYTES: 覆蓋預設的 Docker mtu 設定
可以在設定檔案中設定 DOCKER_OPTS,並重啟服務。由於目前 Docker 橋接器是 Linux 橋接器,使用者可以使用 brctl show 來查看橋接器和連接埠連接訊息。


*註: brctl 命令在 Debian、Ubuntu 中可以使用 sudo apt-get install bridge-utils 來安裝。

每次建立一個新容器的時候,Docker 從可用的位址段中選擇一個未使用的 IP 位址分配給容器的 eth0 連接埠。使用本地主機上 docker0 界面的 IP 作為所有容器的預設網關。一個範例如下:
# docker run -i -t --rm base /bin/bash 
# ip addr show eth0 | egrep "inet.*eth0" 
inet 172.17.0.4/16 scope global eth0 
# ip route 
default via 172.17.42.1 dev eth0 
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.4

自定義橋接器#

(P64) 除了預設的 docker0 橋接器,使用者也可以指定橋接器來連接各個容器。

在啟動 Docker 服務的時候,使用 -b BRIDGE 或 --bridge=BRIDGE 來指定使用的橋接器。

如果服務已經執行,那需要先停止服務,並刪除舊的橋接器, 作法如下:
# service docker stop 
# ip link set dev docker0 down 
# brctl delbr docker0

然後建立一個橋接器 bridge0:
# brctl addbr bridge0 
# ip addr add 192.168.5.1/24 dev bridge0 
# ip link set dev bridge0 up

查看確認橋接器建立並啟動:


設定 Docker 服務,預設橋接到建立的橋接器上:
# echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker 
# service docker start

啟動 Docker 服務。 新建一個容器,可以看到它已經橋接到了 bridge0 上; 可以繼續用 brctl show 命令查看橋接的訊息;另外在容器中可以使用 ip addr 和 ip route命令來查看 IP 位址設定和路由訊息。 

工具和示例#

在介紹自定義網路拓撲之前,你可能會對一些外部工具和例子感興趣:
  • pipework: Jérôme Petazzoni 編寫了一個叫 pipework 的 shell 腳本,可以幫助使用者在比較復雜的場景中完成容器的連接。
Pipework lets you connect together containers in arbitrarily complex scenarios. Pipework uses cgroups and namespace and works with "plain" LXC containers (created with lxc-start), and with the awesome Docker.
  • playground: Brandon Rhodes 建立了一個提供完整的 Docker 容器網路拓撲管理的 Python庫,包括路由、NAT 防火墻;以及一些提供 HTTP, SMTP, POP, IMAP, Telnet, SSH, FTP 的伺服器。

編輯網路設定檔案#

Docker 1.2.0 開始支援在執行中的容器裡編輯 /etc/hosts , /etc/hostname 和 /etc/resolve.conf 檔案。但是這些修改是臨時的,只在執行的容器中保留,容器終止或重啟後並不會被保存下來。也不會被 docker commit 提交。

範例 - 建立一個點到點連接#

預設情況下,Docker 會將所有容器連接到由 docker0 提供的虛擬子網中。使用者有時候需要兩個容器之間可以直連通信,而不用透過主機橋接器進行橋接。解決辦法很簡單:建立一對 peer 界面,分別放到兩個容器中,設定成點到點鏈路類型即可。

首先啟動 2 個容器:
# docker run -it --rm --net=none johnklee/tutorial /bin/bash 
root@a8b7390cb531:/# // 按下 Ctrl+p, Ctrl+q 回到 Host 
# docker run -it --rm --net=none johnklee/tutorial /bin/bash 
root@54af613ad065:/# // 按下 Ctrl+p, Ctrl+q 回到 Host 

找到程式號,然後建立網路命名空間的跟蹤檔案:
# docker inspect -f '{{.State.Pid}}' 54af613ad065 
6170 
# docker inspect -f '{{.State.Pid}}' a8b7390cb531 
6140 
# mkdir -p /var/run/netns 
# ln -s /proc/6170/ns/net /var/run/netns/6170 
# ln -s /proc/6140/ns/net /var/run/netns/6140

建立一對 peer 界面,然後設定路由:
# ip link add A type veth peer name B 
# ip link set A netns 6170 
# ip netns exec 6170 ip addr add 10.1.1.1/32 dev A 
# ip netns exec 6170 ip link set A up 
# ip netns exec 6170 ip route add 10.1.1.2/32 dev A 
# ip link set B netns 6140 
# ip netns exec 6140 ip addr add 10.1.1.2/32 dev B 
# ip netns exec 6140 ip link set B up 
# ip netns exec 6140 ip route add 10.1.1.1/32 dev B

現在這 2 個容器就可以相互 ping 通,並成功建立連接。點到點鏈路不需要子網和子網遮罩。此外,也可以不指定 --net=none 來建立點到點鏈路。這樣容器還可以透過原先的網路來通信。

利用類似的辦法,可以建立一個只跟主機通信的容器。但是一般情況下,更推薦使用 --icc=false 來關閉容器之間的通信。

補充說明#

When Docker starts, it creates a virtual interface named docker0 on the host machine. It randomly chooses an address and subnet from the private range defined by RFC 1918 that are not in use on the host machine, and assigns it to docker0...
To install the latest version of Docker, use the standard -N flag with wget:
$ wget -N https://get.docker.com/ | sh
Show and manipulate routing, devices, policy routing and tunnels.

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...