├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README-ZH.md ├── README.md ├── accessor ├── README-ZH.md ├── README.md ├── go.mod ├── go.sum ├── main.go ├── setup_darwin.go ├── setup_linux.go └── setup_windows.go ├── desktop ├── README.md ├── config.go ├── expose.go ├── go.mod ├── go.sum ├── main.go ├── net_darwin.go ├── net_windows.go ├── options.conf.template ├── proxy.go ├── service.go └── tools │ ├── install-service.bat │ ├── restart-service.bat │ ├── start-connector.bat │ ├── start-service.bat │ ├── stop-service.bat │ └── uninstall-service.bat └── docker ├── Dockerfile ├── README.md ├── dns_server.go ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | docker/desktop-connector 18 | desktop/docker-connector 19 | desktop/docker-connector-*.tar.gz 20 | desktop/options.conf 21 | desktop/docker-connector-*-sha256.txt 22 | desktop/build 23 | desktop/*.zip 24 | accessor/build 25 | accessor/docker-accessor 26 | accessor/docker-accessor*.tar.gz 27 | accessor/docker-accessor*.zip 28 | accessor/docker-accessor-*-sha256.txt 29 | tmp/ 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "gopls": { 3 | "experimentalWorkspaceModule": true 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wenjun Xiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/wenjunxiao/mac-docker-connector/blob/master/README.md) | [中文简体](https://github.com/wenjunxiao/mac-docker-connector/blob/master/README-ZH.md) 2 | 3 | > 把`mac-docker-connector`重命名为`desktop-docker-connector`是为了同时支持Mac和Widnwos下的Docker 4 | # desktop-docker-connector 5 | 6 | `Docker Desktop for Mac and Windows` 没有提供从宿主的macOS或Windows通过容器IP访问容器的方式。参考[Known limitations, use cases, and workarounds](https://docs.docker.com/docker-for-mac/networking/#i-cannot-ping-my-containers)。通过一个[复杂解决方法](https://pjw.io/articles/2018/04/25/access-to-the-container-network-of-docker-for-mac/)得到灵感,主要方式在宿主的macOS和Docker的Hypervisor之间建立一个VPN 7 | ``` 8 | +---------------+ +--------------------+ 9 | | | | Hypervisor/Hyper-V | 10 | | macOS/Windows | | +-----------+ | 11 | | | | | Container | | 12 | | | vpn | +-----------+ | 13 | | VPN Client |<-------->| VPN Server | 14 | +---------------+ +--------------------+ 15 | ``` 16 | 但是宿主的macOS无法直接访问Hypervisor,VPN服务容器需要使用`host`以便与Hypervisor在同一网络环境中,必须使用一个转发容器(比如`socat`)导出端口到macOS,然后转发到VPN服务。考虑到VPN连接的双工的,因此我们可以把VPN服务和客户端反转一下,变成下面的结构 17 | ``` 18 | +---------------+ +--------------------+ 19 | | | | Hypervisor/Hyper-V | 20 | | macOS/Windows | | +-----------+ | 21 | | | | | Container | | 22 | | | vpn | +-----------+ | 23 | | VPN Server |<-------->| VPN Client | 24 | +---------------+ +--------------------+ 25 | ``` 26 | 尽管如此, 我们需要做更多额外的工作来使用openvpn,比如证书、配置等。 27 | 这对于只是通过IP访问容器的需求来说,这些工作略显麻烦。 28 | 我们只需要建立一个连接通道,无需证书,也可以无需客户端 29 | ``` 30 | +---------------+ +--------------------+ 31 | | | | Hypervisor/Hyper-V | 32 | | macOS/Windows | | +-----------+ | 33 | | | | | Container | | 34 | | | udp | +-----------+ | 35 | | TUN Server |<-------->| TUN Client | 36 | +---------------+ +--------------------+ 37 | ``` 38 | 鉴于Docker官方文档[Docker and iptables](https://docs.docker.com/network/iptables/)中描述那样, 39 | 两个子网之间的互通性有时也是需要的,因此还可以通过`iptables`来提供两个子网之间的互相连接 40 | ``` 41 | +-------------------------------+ 42 | | Hypervisor/Hyper-V | 43 | | +----------+ +----------+ | 44 | | | subnet 1 |<--->| subnet 2 | | 45 | | +----------+ +----------+ | 46 | +-------------------------------+ 47 | ``` 48 | 49 | ## 使用 50 | 51 | ### 宿主机 52 | 53 | #### Mac 54 | 先安装Mac端的服务`mac-docker-connector` 55 | ```bash 56 | $ brew tap wenjunxiao/brew 57 | $ brew install docker-connector 58 | ``` 59 | 60 | 首次配置通过以下命令把所有Docker所有`bridge`子网放入配置文件,后续的增减可以参考后面的详细配置 61 | ```bash 62 | $ docker network ls --filter driver=bridge --format "{{.ID}}" | xargs docker network inspect --format "route {{range .IPAM.Config}}{{.Subnet}}{{end}}" >> "$(brew --prefix)/etc/docker-connector.conf" 63 | ``` 64 | 65 | 启动Mac端的服务 66 | ```bash 67 | $ sudo brew services start docker-connector 68 | ``` 69 | 70 | 安装Docker端的容器`mac-docker-connector` 71 | ```bash 72 | $ docker pull wenjunxiao/mac-docker-connector 73 | ``` 74 | 75 | #### Windows 76 | 77 | 从[Releases](https://github.com/wenjunxiao/desktop-docker-connector/releases)下载 `desktop-docker-connector`然后解压. 78 | 79 | 执行以下命令安装服务,把所有需要访问的Bridge子网地址按照`route 172.17.0.0/16`的格式写入`options.conf` 80 | ```cmd 81 | $ desktop-docker-connector.exe install -config options.conf 82 | ``` 83 | 84 | 把所有需要访问的Bridge子网地址按照`route 172.17.0.0/16`的格式写入`options.conf` 85 | ```conf 86 | route 172.17.0.0/16 87 | ``` 88 | 可以通过脚本`start-connector.bat`来直接启动连接器,或者把连接器按照以下步骤安装成服务之后启动: 89 | 1. 运行脚本`install-service.bat`安装服务. 90 | 2. 运行脚本`start-service.bat`来启动服务. 91 | 还可以通过运行脚本`stop-service.bat`停止服务以及运行脚本`uninstall-service.bat`卸载服务 92 | 93 | ### Docker 94 | 95 | 启动Docker端的容器,其中网络必须是`host`,并且添加`NET_ADMIN`特性 96 | ```bash 97 | $ docker run -it -d --restart always --net host --cap-add NET_ADMIN --name mac-connector wenjunxiao/mac-docker-connector 98 | ``` 99 | 100 | 如果你向导出你自己的容器给其他人,让其他人可以访问你在容器中搭建的服务,其他人必须安装另一个客户端[docker-accessor](./accessor),同时你必须开启`expose`(这默认是关闭的)和提供访问的令牌(`token`), 101 | 更详细的配置说明参考配置说明 102 | 103 | ## 配置说明 104 | 105 | 基本的配置选项,通常你不需要修改他们,除非你的环境冲突(比如端口被占用,子网已使用)。 106 | 一旦需要变更,那么Docker容器`mac-docker-connector`也需要使用相同的参数重新启动 107 | * `addr` 虚拟网络地址, 默认 `192.168.251.1/24`(可以修改,但容器端需要同步修改参数) 108 | ``` 109 | addr 192.168.251.1/24 110 | ``` 111 | * `port` UDP服务监听端口, 默认`2511`(可以修改,但容器端需要同步修改参数) 112 | ``` 113 | port 2511 114 | ``` 115 | * `mtu` 网络的MTU值,默认`1400`(可以修改,但容器端需要同步修改参数) 116 | ``` 117 | mtu 1400 118 | ``` 119 | * `host` UDP监听的地址,仅用于Docker容器`mac-docker-connector`连接使用,处于安全和适应移动办公设置成`127.0.0.1`(通常无需修改) 120 | ``` 121 | host 127.0.0.1 122 | ``` 123 | 124 | 动态热加载的配置选项,修改配置文件之后无需启动,立即生效(除非禁用`watch`),可以在需要的时候随时增减 125 | * `route` 添加一条访问Docker容器子网的的路由,通常在你通过`docker network create --subnet x.x.x.x/mask name`命令创建一个`bridge`子网时需要添加 126 | ``` 127 | route 172.100.0.0/16 128 | ``` 129 | * `iptables` 插入(`+`)或删除(`-`)一条`iptables`规则,用于两个子网之间互相访问 130 | ``` 131 | iptables 172.0.1.0+172.0.2.0 132 | iptables 172.0.3.0-172.0.4.0 133 | ``` 134 | IP是无掩码子网的地址,通过`+`连接表示插入一条可以互相访问的规则,通过`-`连接表示删除它们之间互相访问的规则 135 | * `expose` 导出你本地的容器给其他人,指定其他人用于连接的开放端口 136 | ``` 137 | expose 0.0.0.0:2512 138 | ``` 139 | 导出的地址必须是其他人可以通过[docker-accessor](./accessor)访问的地址 140 | * `token` 定义其他人访问你的服务的令牌,以及连接成功之后分配的虚拟网络IP 141 | ``` 142 | token token-name 192.168.251.3 143 | ``` 144 | 令牌是自定义的字符串,并且在配置文件中唯一,IP则必须是`addr`配置的虚拟网络中有效的IP 145 | * `hosts` 让本地自定义`127.0.0.1`对应的域名也可以在容器中使用 146 | ``` 147 | hosts /etc/hosts .local .inc 148 | ``` 149 | 第一个参数是hosts文件,后续的参数是过滤的域名后缀 150 | * `proxy` 让本地监听`127.0.0.1`的服务也可以被容器访问 151 | ``` 152 | proxy 127.0.0.1:80:80 153 | ``` 154 | 第一部分`127.0.0.1:80`是本地服务监听的地址,后面部分的端口`80`是代理监听的端口 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/wenjunxiao/mac-docker-connector/blob/master/README.md) | [中文简体](https://github.com/wenjunxiao/mac-docker-connector/blob/master/README-ZH.md) 2 | 3 | > Change mac-docker-connector to desktop-docker-connector to support both Docker Desktop for Mac and Docker Desktop for Windows 4 | # desktop-docker-connector 5 | 6 | `Docker Desktop for Mac and Windows` does not provide access to container IP from host(macOS or Windows). 7 | Reference [Known limitations, use cases, and workarounds](https://docs.docker.com/docker-for-mac/networking/#i-cannot-ping-my-containers). 8 | There is a [complex solution](https://pjw.io/articles/2018/04/25/access-to-the-container-network-of-docker-for-mac/), 9 | which is also my source of inspiration. The main idea is to build a VPN between the macOS/Windows host and the docker virtual machine. 10 | ``` 11 | +---------------+ +--------------------+ 12 | | | | Hypervisor/Hyper-V | 13 | | macOS/Windows | | +-----------+ | 14 | | | | | Container | | 15 | | | vpn | +-----------+ | 16 | | VPN Client |<-------->| VPN Server | 17 | +---------------+ +--------------------+ 18 | ``` 19 | But the macOS/Windows host cannot access the container, the vpn port must be exported and forwarded. 20 | Since the VPN connection is duplex, so we can reverse it. 21 | ``` 22 | +---------------+ +--------------------+ 23 | | | | Hypervisor/Hyper-V | 24 | | macOS/Windows | | +-----------+ | 25 | | | | | Container | | 26 | | | vpn | +-----------+ | 27 | | VPN Server |<-------->| VPN Client | 28 | +---------------+ +--------------------+ 29 | ``` 30 | Even so, we need to do more extra work to use openvpn, such as certificates, configuration, etc. 31 | All I want is to access the container via IP, why is it so cumbersome. 32 | No need for security, multi-clients, or certificates, just connect. 33 | ``` 34 | +---------------+ +--------------------+ 35 | | | | Hypervisor/Hyper-V | 36 | | macOS/Windows | | +-----------+ | 37 | | | | | Container | | 38 | | | udp | +-----------+ | 39 | | TUN Server |<-------->| TUN Client | 40 | +---------------+ +--------------------+ 41 | ``` 42 | In the view of [Docker and iptables](https://docs.docker.com/network/iptables/), 43 | this tool also provides the ability of two subnets to access each other. 44 | ``` 45 | +-------------------------------+ 46 | | Hypervisor/Hyper-V | 47 | | +----------+ +----------+ | 48 | | | subnet 1 |<--->| subnet 2 | | 49 | | +----------+ +----------+ | 50 | +-------------------------------+ 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Host 56 | #### MacOS 57 | 58 | Install mac client of `desktop-docker-connector`. 59 | ```bash 60 | $ brew tap wenjunxiao/brew 61 | $ brew install docker-connector 62 | ``` 63 | 64 | Config route of docker network 65 | ```bash 66 | $ docker network ls --filter driver=bridge --format "{{.ID}}" | xargs docker network inspect --format "route {{range .IPAM.Config}}{{.Subnet}}{{end}}" >> "$(brew --prefix)/etc/docker-connector.conf" 67 | ``` 68 | 69 | Start the service 70 | ```bash 71 | $ sudo brew services start docker-connector 72 | ``` 73 | 74 | #### Windows 75 | 76 | Need to install tap driver [tap-windows](http://build.openvpn.net/downloads/releases/) from [OpenVPN](https://community.openvpn.net/openvpn/wiki/ManagingWindowsTAPDrivers). 77 | Download the latest version `http://build.openvpn.net/downloads/releases/latest/tap-windows-latest-stable.exe` and install. 78 | 79 | Download windows client of `desktop-docker-connector` from [Releases](https://github.com/wenjunxiao/desktop-docker-connector/releases), and then unzip it. 80 | 81 | Append bridge network to `options.conf`, format like `route 172.17.0.0/16`. 82 | ```conf 83 | route 172.17.0.0/16 84 | ``` 85 | Run directly by bat `start-connector.bat` or install as service by follow step: 86 | 1. Run the bat `install-service.bat` to install as windows service. 87 | 2. Run the bat `start-service.bat` to start the connector service. 88 | And finally, you can run the bat `stop-service.bat` to stop the connector service, 89 | run the bat `uninstall-service.bat` to uninstall the connector service. 90 | 91 | ### Docker 92 | 93 | Install docker front of `desktop-docker-connector` 94 | ```bash 95 | $ docker pull wenjunxiao/desktop-docker-connector 96 | ``` 97 | 98 | Start the docker front. The network must be `host`, and add `NET_ADMIN` capability. 99 | 100 | ```bash 101 | $ docker run -it -d --restart always --net host --cap-add NET_ADMIN --name desktop-connector wenjunxiao/desktop-docker-connector 102 | ``` 103 | 104 | If you want to expose the containers of docker to other pepole, Please reference [docker-accessor](./accessor) 105 | 106 | ## Configuration 107 | 108 | Basic configuration items, do not need to modify these, unless your environment conflicts, 109 | if necessary, then the docker container `desktop-docker-connector` also needs to be started with the same parameters 110 | * `addr` virtual network address, default `192.168.251.1/24` (change if it conflict) 111 | ``` 112 | addr 192.168.251.1/24 113 | ``` 114 | * `port` udp listen port, default `2511` (change if it conflict) 115 | ``` 116 | port 2511 117 | ``` 118 | * `mtu` the MTU of network, default `1400` 119 | ``` 120 | mtu 1400 121 | ``` 122 | * `host` udp listen host, used to be connected by `desktop-docker-connector`, default `127.0.0.1` for security and adaptation 123 | ``` 124 | host 127.0.0.1 125 | ``` 126 | 127 | Dynamic hot-loading configuration items can take effect without restarting, 128 | and need to be added or modified according to your needs. 129 | * `route` Add a route to access the docker container subnet, usually when you create a bridge network by `docker network create --subnet 172.56.72.0/24 app`, run `echo "route 172.56.72.0/24" >> "$(brew --prefix)/etc/docker-connector.conf"` to append route to config file. 130 | ``` 131 | route 172.56.72.0/24 132 | ``` 133 | * `iptables` Insert(`+`) or delete(`-`) a iptable rule for two subnets to access each other. 134 | ``` 135 | iptables 172.0.1.0+172.0.2.0 136 | iptables 172.0.3.0-172.0.4.0 137 | ``` 138 | The ip is subnet address without mask, and join with `+` to insert a rule, and join with `-` to delete a rule. 139 | * `expose` Expose you docker container to other pepole, default disabled. 140 | ``` 141 | expose 0.0.0.0:2512 142 | ``` 143 | the exposed address should be connected by [docker-accessor](./accessor). 144 | And then add `expose` after then `route` you want to be exposed 145 | ``` 146 | route 172.100.0.0/16 expose 147 | ``` 148 | * `token` Define the access token and the virtual IP assigned after connection 149 | ``` 150 | token token-name 192.168.251.3 151 | ``` 152 | The token name is customized and unique, and the IP must be valid in the virtual network 153 | defined by `addr` 154 | * `hosts` allows the custom domain with ip `127.0.0.1`, also can be used in the container 155 | ```` 156 | hosts /etc/hosts .local .inc 157 | ```` 158 | The first parameter is the hosts file, and the subsequent parameters are the filtered domain name suffix 159 | * `proxy` allows services that listen locally on `127.0.0.1` to be accessed by the container 160 | ```` 161 | proxy 127.0.0.1:80:80 162 | ```` 163 | The first part `127.0.0.1:80` is the address where the local service listens, and the port `80` in the latter part is the port where the proxy listens -------------------------------------------------------------------------------- /accessor/README-ZH.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | [中文简体](README-ZH.md) 2 | 3 | # docker-accessor 4 | 5 | 连接到macOS上的`docker-connector`,以便于访问macOS导出的容器 6 | 7 | ## 安装 8 | 9 | ### Windows 10 | 11 | 首先需要从[OpenVPN](https://community.openvpn.net/openvpn/wiki/ManagingWindowsTAPDrivers)下载安装tap驱动[tap-windows](http://build.openvpn.net/downloads/releases/)。 12 | 下载最新版本`http://build.openvpn.net/downloads/releases/latest/tap-windows-latest-stable.exe`并安装. 13 | 然后从[Releases](https://github.com/wenjunxiao/mac-docker-connector/releases)下载最新的`docker-accessor` 14 | 15 | ### MacOS 16 | 17 | 通过brew安装 18 | ```bash 19 | $ brew install wenjunxiao/brew/docker-accessor 20 | ``` 21 | 22 | ### Linux 23 | 24 | 从[Releases](https://github.com/wenjunxiao/mac-docker-connector/releases)下载`docker-accessor` 25 | ```bash 26 | $ curl -L -o- https://github.com/wenjunxiao/mac-docker-connector/releases/download/v2.0/docker-accessor-linux.tar.gz | tar -xzf - -C /usr/local/bin 27 | ``` 28 | 29 | ## 使用 30 | 31 | 通过macOS上的`docker-connector`导出的地址和端口,以及分配的令牌启动`docker-accessor` 32 | ```bash 33 | $ docker-accessor -remote 192.168.1.100:2512 -token my-token 34 | ``` 35 | 如果本地存在相同的某些子网,也可以排出对应的子网以避免和本地的子网冲突 36 | ```bash 37 | $ docker-accessor -remote 192.168.1.100:2512 -token my-token -exclude 172.1.0.0/24,172.2.0.0/24 38 | ``` 39 | 40 | ## Compile 41 | 42 | ### Windows 43 | 44 | ```bash 45 | $ GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -tags netgo -o ./build/win/x86_64/docker-accessor.exe . 46 | $ zip -j docker-accessor-win-x86_64.zip ./build/win/x86_64/docker-accessor.exe 47 | $ GOOS=windows GOARCH=386 go build -ldflags "-s -w" -tags netgo -o ./build/win/i686/docker-accessor.exe . 48 | $ zip -j docker-accessor-win-i686.zip ./build/win/i686/docker-accessor.exe 49 | ``` 50 | 51 | ### Linux 52 | 53 | ```bash 54 | $ GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -tags netgo -o ./build/linux/docker-accessor . 55 | $ tar -czf docker-accessor-linux.tar.gz -C ./build/linux docker-accessor 56 | ``` 57 | 58 | ### MacOS 59 | 60 | ```bash 61 | $ GOOS=darwin go build -ldflags "-s -w" -tags netgo -o ./build/darwin/docker-accessor . 62 | $ tar -czf docker-accessor-darwin.tar.gz -C ./build/darwin docker-accessor 63 | $ shasum -a 256 docker-accessor-darwin.tar.gz | awk '{print $1}' > docker-accessor-darwin-sha256.txt 64 | ``` 65 | -------------------------------------------------------------------------------- /accessor/README.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | [中文简体](README-ZH.md) 2 | 3 | # docker-accessor 4 | 5 | Connect to `docker-connector` in macOS and access the exposed container of docker. 6 | 7 | ## Install 8 | 9 | ### Windows 10 | 11 | Need to install tap driver [tap-windows](http://build.openvpn.net/downloads/releases/) from [OpenVPN](https://community.openvpn.net/openvpn/wiki/ManagingWindowsTAPDrivers). 12 | Download the latest version `http://build.openvpn.net/downloads/releases/latest/tap-windows-latest-stable.exe` and install. 13 | Download `docker-accessor` from [Releases](https://github.com/wenjunxiao/mac-docker-connector/releases) 14 | 15 | ### MacOS 16 | 17 | Install by brew. 18 | 19 | ```bash 20 | $ brew install wenjunxiao/brew/docker-accessor 21 | ``` 22 | 23 | ### Linux 24 | 25 | Download `docker-accessor` from [Releases](https://github.com/wenjunxiao/mac-docker-connector/releases) 26 | ```bash 27 | $ curl -L -o- https://github.com/wenjunxiao/mac-docker-connector/releases/download/v2.0/docker-accessor-linux.tar.gz | tar -xzf - -C /usr/local/bin 28 | ``` 29 | 30 | ## Usage 31 | 32 | Run `docker-accessor` with remote `docker-connector` expose address and assigned token. 33 | ```bash 34 | $ docker-accessor -remote 192.168.1.100:2512 -token my-token 35 | ``` 36 | Also can exclude some network of docker to prevent conflict with local. 37 | ```bash 38 | $ docker-accessor -remote 192.168.1.100:2512 -token my-token -exclude 172.1.0.0/24,172.2.0.0/24 39 | ``` 40 | 41 | ## Compile 42 | 43 | ### Windows 44 | 45 | ```bash 46 | $ GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -tags netgo -o ./build/win/x86_64/docker-accessor.exe . 47 | $ zip -j build/docker-accessor-win-x86_64.zip ./build/win/x86_64/docker-accessor.exe 48 | $ GOOS=windows GOARCH=386 go build -ldflags "-s -w" -tags netgo -o ./build/win/i386/docker-accessor.exe . 49 | $ zip -j build/docker-accessor-win-i386.zip ./build/win/i386/docker-accessor.exe 50 | ``` 51 | 52 | ### Linux 53 | 54 | ```bash 55 | $ GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -tags netgo -o ./build/linux/docker-accessor . 56 | $ tar -czf build/docker-accessor-linux.tar.gz -C ./build/linux docker-accessor 57 | ``` 58 | 59 | ### MacOS 60 | 61 | ```bash 62 | $ GOOS=darwin go build -ldflags "-s -w" -tags netgo -o ./build/darwin/docker-accessor . 63 | $ tar -czf build/docker-accessor-darwin.tar.gz -C ./build/darwin docker-accessor 64 | $ shasum -a 256 build/docker-accessor-darwin.tar.gz | awk '{print $1}' > build/docker-accessor-darwin-sha256.txt 65 | ``` 66 | -------------------------------------------------------------------------------- /accessor/go.mod: -------------------------------------------------------------------------------- 1 | module docker-accesor 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.9 7 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 8 | github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 9 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 10 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /accessor/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 2 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 3 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 4 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 5 | github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w= 6 | github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY= 7 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= 8 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= 9 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= 11 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | -------------------------------------------------------------------------------- /accessor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/songgao/water" 15 | ) 16 | 17 | var ( 18 | debug = false 19 | remote = "" 20 | token = "" 21 | exclude = "" 22 | clears = make(map[string]bool) 23 | ) 24 | 25 | func init() { 26 | flag.BoolVar(&debug, "debug", debug, "Provide debug info") 27 | flag.StringVar(&remote, "remote", remote, "remote address") 28 | flag.StringVar(&token, "token", token, "assigned token") 29 | flag.StringVar(&exclude, "exclude", token, "exclude network") 30 | } 31 | 32 | func _runCmd(args string, output bool) (string, error) { 33 | argv := strings.Split(args, " ") 34 | cmd := exec.Command(argv[0], argv[1:]...) 35 | out, err := cmd.CombinedOutput() 36 | if out != nil { 37 | outStr := string(out) 38 | if output { 39 | fmt.Printf("command => %s %v\n", args, outStr) 40 | } else { 41 | fmt.Printf("command => %s\n", args) 42 | } 43 | return outStr, err 44 | } 45 | return "", err 46 | } 47 | 48 | func runCmd(format string, a ...interface{}) (string, error) { 49 | return _runCmd(fmt.Sprintf(format, a...), false) 50 | } 51 | 52 | func runOutCmd(format string, a ...interface{}) (string, error) { 53 | return _runCmd(fmt.Sprintf(format, a...), true) 54 | } 55 | 56 | func main() { 57 | flag.Parse() 58 | udpAddr, err := net.ResolveUDPAddr("udp", remote) 59 | if err != nil { 60 | fmt.Printf("invalid address => %s\n", remote) 61 | os.Exit(1) 62 | } 63 | conn, err := net.DialUDP("udp", nil, udpAddr) 64 | if err != nil { 65 | fmt.Printf("failed to dial %s => %s\n", remote, err.Error()) 66 | os.Exit(1) 67 | } 68 | defer conn.Close() 69 | fmt.Printf("local => %s\n", conn.LocalAddr()) 70 | fmt.Printf("remote => %s\n", conn.RemoteAddr()) 71 | conn.Write([]byte(fmt.Sprintf("%s%s", string([]byte{0}), token))) 72 | data := make([]byte, 2000) 73 | ready := make(chan bool) 74 | exiting := false 75 | var iface *water.Interface 76 | go func() { 77 | <-ready 78 | buf := make([]byte, 2000) 79 | for { 80 | n, err := iface.Read(buf) 81 | if err != nil { 82 | if exiting { 83 | break 84 | } 85 | fmt.Printf("tun read error: %v\n", err) 86 | time.Sleep(time.Second) 87 | continue 88 | } 89 | if _, err := conn.Write(buf[:n]); err != nil { 90 | fmt.Printf("udp write error: %v\n", err) 91 | } 92 | } 93 | }() 94 | logged := false 95 | go func() { 96 | for { 97 | n, err := conn.Read(data) 98 | if err != nil { 99 | if exiting { 100 | break 101 | } 102 | fmt.Printf("failed read udp msg: %d %v\n", n, err.Error()) 103 | if n == 0 { 104 | time.Sleep(time.Second) 105 | continue 106 | } 107 | } 108 | if logged { 109 | if _, err := iface.Write(data[:n]); err != nil { 110 | if data[0] == 2 && n == 1 { // 未认证 111 | logged = false 112 | fmt.Println("relogin") 113 | conn.Write([]byte(fmt.Sprintf("%s%s", string([]byte{1}), token))) 114 | } else if n > 0 { 115 | fmt.Printf("tun write error: %v\n", err) 116 | } 117 | } 118 | } else { 119 | if data[0] == 1 { 120 | logged = true 121 | fmt.Println("logged") 122 | if iface != nil { 123 | for cm := range clears { 124 | runCmd(cm) 125 | delete(clears, cm) 126 | } 127 | iface.Close() 128 | time.Sleep(time.Second) 129 | iface = setup(strings.Split(string(data[1:n]), ",")) 130 | } else { 131 | iface = setup(strings.Split(string(data[1:n]), ",")) 132 | ready <- true 133 | } 134 | } else if data[0] == 2 { // 未认证 135 | logged = false 136 | fmt.Println("relogin") 137 | conn.Write([]byte(fmt.Sprintf("%s%s", string([]byte{1}), token))) 138 | } 139 | } 140 | } 141 | }() 142 | c := make(chan os.Signal, 1) 143 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 144 | s := <-c 145 | fmt.Println("exit signal =>", s) 146 | exiting = true 147 | for cm := range clears { 148 | runCmd(cm) 149 | } 150 | iface.Close() 151 | } 152 | -------------------------------------------------------------------------------- /accessor/setup_darwin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | "github.com/songgao/water" 10 | ) 11 | 12 | func setup(ips []string) *water.Interface { 13 | addr := strings.Split(ips[0], " ")[1] 14 | ip, _, err := net.ParseCIDR(addr) 15 | if err != nil { 16 | fmt.Println(err) 17 | os.Exit(1) 18 | } 19 | peer := strings.Split(ips[1], " ")[1] 20 | config := water.Config{ 21 | DeviceType: water.TUN, 22 | } 23 | iface, err := water.New(config) 24 | if err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | fmt.Printf("interface => %v\n", iface.Name()) 29 | runOutCmd("ifconfig %s inet %s %s netmask 255.255.255.255 up", iface.Name(), ip, peer) 30 | for _, val := range ips { 31 | vals := strings.Split(val, " ") 32 | fmt.Printf("control => %s\n", val) 33 | if vals[0] == "route" { 34 | if _, _, err := net.ParseCIDR(vals[1]); err != nil { 35 | fmt.Printf("unknown route => %v\n", vals[1]) 36 | } else if strings.Contains(exclude, vals[1]) { 37 | fmt.Printf("exclude route => %v\n", vals[1]) 38 | } else { 39 | rk := fmt.Sprintf("-net %s %s", vals[1], ip) 40 | clears[fmt.Sprintf("route -n delete %s", rk)] = true 41 | runCmd("route -n add %s", rk) 42 | } 43 | } 44 | } 45 | return iface 46 | } 47 | -------------------------------------------------------------------------------- /accessor/setup_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | "github.com/songgao/water" 10 | ) 11 | 12 | func setup(ips []string) *water.Interface { 13 | addr := strings.Split(ips[0], " ")[1] 14 | ip, _, err := net.ParseCIDR(addr) 15 | if err != nil { 16 | fmt.Println(err) 17 | os.Exit(1) 18 | } 19 | peer := strings.Split(ips[1], " ")[1] 20 | config := water.Config{ 21 | DeviceType: water.TUN, 22 | } 23 | iface, err := water.New(config) 24 | if err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | fmt.Printf("interface => %v\n", iface.Name()) 29 | runCmd("%s link set dev %s up qlen 100", "ip", iface.Name()) 30 | runCmd("%s addr add dev %s local %s peer %s", "ip", iface.Name(), ip, peer) 31 | for _, val := range ips { 32 | vals := strings.Split(val, " ") 33 | fmt.Printf("control => %s\n", val) 34 | if vals[0] == "route" { 35 | if _, _, err := net.ParseCIDR(vals[1]); err != nil { 36 | fmt.Printf("unknown route => %v\n", vals[1]) 37 | } else if strings.Contains(exclude, vals[1]) { 38 | fmt.Printf("exclude route => %v\n", vals[1]) 39 | } else { 40 | rk := fmt.Sprintf("%s via %s dev %s", vals[1], ip, iface.Name()) 41 | clears[fmt.Sprintf("ip route del %s", rk)] = true 42 | runCmd("ip route add %s", rk) 43 | } 44 | } else if vals[0] == "mtu" { 45 | runCmd("%s link set dev %s mtu %s", "ip", iface.Name(), vals[1]) 46 | } 47 | } 48 | 49 | return iface 50 | } 51 | -------------------------------------------------------------------------------- /accessor/setup_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/songgao/water" 11 | ) 12 | 13 | func setup(ips []string) *water.Interface { 14 | addr := strings.Split(ips[0], " ")[1] 15 | ip, subnet, err := net.ParseCIDR(addr) 16 | if err != nil { 17 | fmt.Println(err) 18 | os.Exit(1) 19 | } 20 | peer := strings.Split(ips[1], " ")[1] 21 | mask := net.IP(subnet.Mask).String() 22 | config := water.Config{ 23 | DeviceType: water.TUN, 24 | PlatformSpecificParams: water.PlatformSpecificParams{ 25 | ComponentID: "tap0901", 26 | Network: addr, 27 | }, 28 | } 29 | iface, err := water.New(config) 30 | if err != nil { 31 | fmt.Println(err) 32 | os.Exit(1) 33 | } 34 | fmt.Printf("interface => \"%s\"\n", iface.Name()) 35 | runCmd(fmt.Sprintf("netsh interface ip set address \"%s\" static %s %s %s", iface.Name(), ip, mask, peer)) 36 | ipStr := ip.String() 37 | for { 38 | if out, err := runCmd(fmt.Sprintf("netsh interface ip show addresses \"%s\"", iface.Name())); err == nil { 39 | if strings.Contains(out, ipStr) && strings.Contains(out, mask) { 40 | break 41 | } else { 42 | fmt.Println("waiting network setup...") 43 | time.Sleep(time.Second) 44 | } 45 | } else { 46 | break 47 | } 48 | } 49 | runCmd("netsh interface ip delete dns \"%s\" all", iface.Name()) 50 | runCmd("netsh interface ip delete wins \"%s\" all", iface.Name()) 51 | for _, val := range ips { 52 | vals := strings.Split(val, " ") 53 | fmt.Printf("control => %s\n", val) 54 | if vals[0] == "route" { 55 | ip, subnet, err := net.ParseCIDR(vals[1]) 56 | if err != nil { 57 | fmt.Printf("unknown route => %v\n", vals[1]) 58 | } else if strings.Contains(exclude, vals[1]) { 59 | fmt.Printf("exclude route => %v\n", vals[1]) 60 | } else { 61 | rk := fmt.Sprintf("%s mask %s %s", ip, net.IP(subnet.Mask).String(), peer) 62 | clears[fmt.Sprintf("route delete %s", rk)] = true 63 | runCmd(fmt.Sprintf("route add %s", rk)) 64 | } 65 | } 66 | } 67 | return iface 68 | } 69 | -------------------------------------------------------------------------------- /desktop/README.md: -------------------------------------------------------------------------------- 1 | # docker-connector 2 | 3 | Accept connection from [desktop-connector](../docker) in docker, and route container's ip to it. 4 | Also can expose the access capabilities of Docker containers to others who use [docker-accessor](../accessor) 5 | 6 | ## Install 7 | 8 | You can install from [brew](https://github.com/wenjunxiao/homebrew-brew) 9 | ```bash 10 | $ brew install wenjunxiao/brew/docker-connector 11 | ``` 12 | Or download the latest version directly from [release](https://github.com/wenjunxiao/mac-docker-connector/releases) 13 | ```bash 14 | # change the version to latest version 15 | $ curl -sSL -o- https://github.com/wenjunxiao/mac-docker-connector/releases/download/v1.0/docker-connector-mac.tar.gz | tar -zxf - -C /usr/local/bin/ 16 | ``` 17 | 18 | ## Usage 19 | 20 | If install by brew, just start as a service. 21 | ```bash 22 | $ sudo brew services start docker-connector 23 | ``` 24 | Add routes which container subnets you want to access for the first time, 25 | and you can add or delete later. 26 | You can add all bridge subnet by `docker network ls --filter driver=bridge` 27 | ```bash 28 | $ docker network ls --filter driver=bridge --format "{{.ID}}" | xargs docker network inspect --format "route {{range .IPAM.Config}}{{.Subnet}}{{end}}" >> "$(brew --prefix)/etc/docker-connector.conf" 29 | ``` 30 | Or just add specified subnet route you like 31 | ```bash 32 | $ cat <> "$(brew --prefix)/etc/docker-connector.conf" 33 | route 172.100.0.0/16 34 | EOF 35 | ``` 36 | 37 | Start with the specified configuration file 38 | ```bash 39 | $ sudo ls # cache sudo password 40 | $ nohup sudo ./docker-connector -config "$(brew --prefix)/etc/docker-connector.conf" & 41 | ``` 42 | 43 | ### Expose access 44 | 45 | You can expose the containers to others, so that they can access the network you built in docker. 46 | Add expose listen address and access tokens. 47 | ```bash 48 | $ cat <> "$(brew --prefix)/etc/docker-connector.conf" 49 | expose 0.0.0.0:2512 50 | token user1 192.168.251.3 51 | token user2 192.168.251.4 52 | EOF 53 | ``` 54 | And append `expose` the route which you want to expose to others. 55 | ```conf 56 | route 172.100.0.0/16 expose 57 | ``` 58 | 59 | For test, you can turn on `pong` to intercept ping requests(only IPv4) 60 | ```bash 61 | $ cat <> "$(brew --prefix)/etc/docker-connector.conf" 62 | pong on 63 | EOF 64 | ``` 65 | 66 | ### Hosts 67 | 68 | A simple DNS server for docker containers, which use the hosts file and filtered by domain suffix, 69 | such as `.local` 70 | ```conf 71 | hosts /etc/hosts .local .local1 72 | ``` 73 | It will usefull for a domain with ip `127.0.0.1`, which will be resolved as another avaliable ip for container, such as `192.168.251.2`. So, you can use any custom domain (`/etc/hosts` for Mac or `C:\Windows\System32\drivers\etc\hosts` for Windows) both in host and conatiner, 74 | ```conf 75 | 127.0.0.1 api.example.local 76 | ``` 77 | You can use comment to force ignore (`docker-connector:ignore`) or include (`docker-connector:resolve`) the entry. 78 | 79 | ```conf 80 | 127.0.0.1 ignore.example.local # docker-connector:ignore 81 | 127.0.0.1 api.example.resolve # docker-connector:resolve 82 | ``` 83 | 84 | ### Proxy 85 | 86 | A simple proxy server for a tcp service with host `127.0.0.1`. It will be usefull for a service, 87 | which is only accessible by the host and the container 88 | ```conf 89 | proxy 127.0.0.1:80:80 90 | ``` 91 | The first part `host:port` is the service listening, and the last port `80` is the proxy listening. 92 | 93 | ## Compile 94 | 95 | ```bash 96 | $ go env -w GOPROXY=https://goproxy.cn,direct 97 | $ go build -tags netgo -o docker-connector . 98 | ``` 99 | 100 | ## Publish 101 | 102 | Publish this to [Homebrew](https://brew.sh/) for easy installation. 103 | 104 | ### Release 105 | 106 | Build and make a tarball for Mac 107 | ```bash 108 | $ go build -tags netgo -o ./build/darwin/docker-connector . 109 | $ tar -czf build/docker-connector-darwin.tar.gz -C ./build/darwin docker-connector 110 | $ shasum -a 256 build/docker-connector-darwin.tar.gz | awk '{print $1}' > build/docker-connector-darwin-sha256.txt 111 | ``` 112 | Build and make a zip for Windows 113 | ```bash 114 | $ GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -tags netgo -o ./build/win/x86_64/docker-connector/docker-connector.exe . 115 | $ cat options.conf.template > ./build/win/x86_64/docker-connector/options.conf.sample 116 | $ cp tools/* ./build/win/x86_64/docker-connector/ 117 | $ cd ./build/win/x86_64/ && zip -r docker-connector-win-x86_64.zip docker-connector && cd ../../../ 118 | $ GOOS=windows GOARCH=386 go build -ldflags "-s -w" -tags netgo -o ./build/win/i386/docker-connector/docker-connector.exe . 119 | $ cat options.conf.template > ./build/win/i386/docker-connector/options.conf.sample 120 | $ cp tools/* ./build/win/i386/docker-connector/ 121 | $ cd ./build/win/i386/ && zip -r docker-connector-win-i386.zip docker-connector && cd ../../../ 122 | ``` 123 | Upload the tarball to [Releases](https://github.com/wenjunxiao/mac-docker-connector/releases) 124 | 125 | ### Homebrew 126 | 127 | Create a ruby repository named `homebrew-brew`, which must start with `homebrew-`. 128 | Clone it and add formula named [docker-connector.rb](https://github.com/wenjunxiao/homebrew-brew/blob/master/Formula/docker-connector.rb) in `Formula` 129 | ```bash 130 | $ git clone https://github.com/wenjunxiao/homebrew-brew 131 | $ cd homebrew-brew 132 | $ mkdir Formula && cd Formula 133 | $ cat < docker-connector.rb 134 | class DockerConnector < Formula 135 | url https://github.com/wenjunxiao/mac-docker-connector/releases/download/x.x.x/docker-connector-mac.tar.gz 136 | sha256 ... 137 | version ... 138 | def install 139 | bin.install "docker-connector" 140 | end 141 | def plist 142 | <<~EOS 143 | ... 144 | EOS 145 | end 146 | end 147 | EOF 148 | $ cd ../ 149 | $ git add . && git commit -m "..." 150 | $ git push origin master 151 | ``` 152 | You can install by brew. 153 | ```bash 154 | $ brew install wenjunxiao/brew/docker-connector 155 | ``` 156 | In addition to github, it can be stored in other warehouses, 157 | and other protocols can also be used. Such as [gitee.com](https://gitee.com/wenjunxiao/homebrew-brew). 158 | You need to specify the full path when installing 159 | ```bash 160 | $ brew tap wenjunxiao/brew https://gitee.com/wenjunxiao/homebrew-brew 161 | $ brew install docker-connector 162 | ``` 163 | If it has already been tapped, you can change remote 164 | ```bash 165 | $ cd `brew --repo`/Library/Taps/wenjunxiao/homebrew-brew 166 | $ git remote set-url origin https://gitee.com/wenjunxiao/homebrew-brew.git 167 | $ brew install docker-connector 168 | ``` 169 | 170 | ## Dev 171 | 172 | Run `main.go` without default config in debug mode. 173 | ```bash 174 | $ sudo go run main.go -port 2521 -addr 192.168.252.1/24 -cli false 175 | ``` 176 | 177 | ## References 178 | 179 | * [Internet Protocol](https://www.ietf.org/rfc/rfc791) -------------------------------------------------------------------------------- /desktop/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net" 10 | "os" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/op/go-logging" 16 | "github.com/songgao/water" 17 | ) 18 | 19 | func normalizeAddr(addr string) string { 20 | if strings.Index(addr, "[::]") == 0 { 21 | return strings.Replace(addr, "[::]", "0.0.0.0", 1) 22 | } 23 | return addr 24 | } 25 | 26 | func isSameAddr(addr0, addr1 string) bool { 27 | if addr0 == addr1 { 28 | return true 29 | } 30 | return normalizeAddr(addr0) == normalizeAddr(addr1) 31 | } 32 | 33 | func loadConfig(iface *water.Interface, init bool) *water.Interface { 34 | fi, err := os.Open(configFile) 35 | if err != nil { 36 | logger.Error("load config failed", err) 37 | return iface 38 | } 39 | defer fi.Close() 40 | re := regexp.MustCompile(`^\s*(\w+\S+)(?:\s+(.*))?$`) 41 | news := make(map[string]bool) 42 | news1 := make(map[string]string) 43 | iptables1 := make(map[string]bool) 44 | if proxyServer != nil { 45 | proxyServer.StartClear() 46 | } 47 | br := bufio.NewReader(fi) 48 | for { 49 | a, _, c := br.ReadLine() 50 | if c == io.EOF { 51 | break 52 | } 53 | s := strings.TrimSpace(string(a)) 54 | match := re.FindStringSubmatch(s) 55 | if match != nil { 56 | val := match[2] 57 | switch match[1] { 58 | case "loglevel": 59 | if level, err := logging.LogLevel(val); err == nil { 60 | logging.SetLevel(level, "vpn") 61 | if leveledBackend != nil { 62 | leveledBackend.SetLevel(level, "vpn") 63 | } 64 | } 65 | case "route": 66 | vals := strings.Split(val, " ") 67 | if len(vals) > 1 { 68 | news[vals[0]] = vals[1] == "expose" 69 | } else { 70 | news[vals[0]] = false 71 | } 72 | case "host": 73 | host = val 74 | case "addr": 75 | addr = val 76 | case "port": 77 | if v, err := strconv.Atoi(val); err == nil { 78 | port = v 79 | } 80 | case "mtu": 81 | if v, err := strconv.Atoi(val); err == nil { 82 | MTU = v 83 | } 84 | case "pong": 85 | pong = val == "on" || val == "true" 86 | case "expose": 87 | restart := strings.Contains(val, "restart") 88 | val = strings.Fields(val)[0] 89 | if udpAddr, err := net.ResolveUDPAddr("udp", val); err == nil { 90 | if expose != nil && (restart || !isSameAddr(expose.LocalAddr().String(), val)) { 91 | logger.Infof("expose changed: %s => %s\n", expose.LocalAddr().String(), val) 92 | expose.Close() 93 | expose = nil 94 | } 95 | if expose == nil { 96 | if expose, err = net.ListenUDP("udp", udpAddr); err != nil { 97 | logger.Warningf("failed to listen => %s\n", val) 98 | } else { 99 | go handleExpose() 100 | } 101 | } 102 | } else { 103 | logger.Warningf("invalid address => %s\n", val) 104 | } 105 | case "token": 106 | vals := strings.Split(val, " ") 107 | news1[vals[0]] = vals[1] 108 | case "iptables": 109 | vals := strings.Split(val, "+") 110 | join := true 111 | if len(vals) == 1 { 112 | vals = strings.Split(val, "-") 113 | join = false 114 | } 115 | val = fmt.Sprintf("%s %s", vals[0], vals[1]) 116 | if vals[0] > vals[1] { 117 | val = fmt.Sprintf("%s %s", vals[1], vals[0]) 118 | } 119 | iptables1[val] = join 120 | case "hosts": 121 | hosts = val 122 | case "proxy": 123 | GetProxyServer().Add(val) 124 | default: 125 | logger.Warningf("unknown action => %s\n", match[1]) 126 | } 127 | } else if s != "" && !strings.HasPrefix(s, "#") { 128 | logger.Warningf("invalid config => %s\n", s) 129 | } 130 | } 131 | if init { 132 | if peer, subnet, err = net.ParseCIDR(addr); err != nil { 133 | logger.Fatal(err) 134 | } 135 | copy([]byte(localIP), []byte(peer.To4())) 136 | localIP[3]++ 137 | if bind { 138 | iface = setup(localIP, peer, subnet) 139 | } 140 | for k, v := range iptables1 { 141 | iptables[k] = v 142 | } 143 | } 144 | if proxyServer != nil { 145 | proxyServer.EndClear() 146 | proxyServer.Start(localIP) 147 | } 148 | logger.Debugf("routes %s => %s\n", map2json(routes), map2json(news)) 149 | for key := range routes { 150 | if val, ok := news[key]; ok { 151 | routes[key] = val 152 | delete(news, key) 153 | } else if bind { 154 | delRoute(key) 155 | } 156 | } 157 | for key := range news { 158 | routes[key] = news[key] 159 | if bind { 160 | delRoute(key) 161 | addRoute(key, peer) 162 | } 163 | } 164 | for key := range tokens { 165 | if v, ok := news1[key]; ok { 166 | tokens[key] = v 167 | } else { 168 | delete(tokens, key) 169 | } 170 | } 171 | for key := range news1 { 172 | tokens[key] = news1[key] 173 | } 174 | for key := range iptables { 175 | if v, ok := iptables1[key]; ok { 176 | if iptables[key] == v { 177 | delete(iptables1, key) 178 | } else { 179 | iptables[key] = v 180 | } 181 | } else { 182 | delete(iptables, key) 183 | iptables1[key] = false 184 | } 185 | } 186 | if cli != nil { 187 | sendControls(cli, iptables1, hosts) 188 | } 189 | news = nil 190 | news1 = nil 191 | iptables1 = nil 192 | return iface 193 | } 194 | 195 | func map2json(m interface{}) string { 196 | if b, err := json.Marshal(m); err == nil { 197 | return string(b) 198 | } 199 | return "" 200 | } 201 | 202 | func appendConfig(data []byte) { 203 | fd, err := os.OpenFile(configFile, os.O_RDWR|os.O_APPEND, 0644) 204 | if err != nil { 205 | return 206 | } 207 | fd.WriteString("\n") 208 | fd.Write(data) 209 | fd.Close() 210 | } 211 | 212 | func sendConfig() { 213 | logger.Infof("send config to => %s:%d\n", host, port) 214 | udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port)) 215 | if err != nil { 216 | return 217 | } 218 | conn, err := net.DialUDP("udp", nil, udpAddr) 219 | if err != nil { 220 | return 221 | } 222 | defer conn.Close() 223 | var data bytes.Buffer 224 | if len(os.Args) > 2 { 225 | data.WriteByte(1) 226 | data.WriteString(strings.Join(os.Args[2:], " ")) 227 | conn.Write(data.Bytes()) 228 | } else { 229 | reader := bufio.NewReader(os.Stdin) 230 | for { 231 | line, hasMore, err := reader.ReadLine() 232 | if err != nil { 233 | break 234 | } 235 | data.Reset() 236 | data.WriteByte(1) 237 | data.Write(line) 238 | conn.Write(data.Bytes()) 239 | if !hasMore { 240 | break 241 | } 242 | } 243 | } 244 | } 245 | 246 | func clearRoutes() { 247 | for key := range routes { 248 | delRoute(key) 249 | } 250 | } 251 | 252 | func loadHosts(buf *bytes.Buffer, hosts string) { 253 | if hosts == "" { 254 | return 255 | } 256 | re := regexp.MustCompile(`^\s*(".*"|\S*)\s+((?:[\w.+-]+\s*){1,})$`) 257 | match := re.FindStringSubmatch(hosts) 258 | if match == nil { 259 | logger.Warningf("invalid hosts config: %v\n", hosts) 260 | return 261 | } 262 | fi, err := os.Open(match[1]) 263 | if err != nil { 264 | logger.Warningf("invalid hosts file: %v\n", match[1]) 265 | return 266 | } 267 | defer fi.Close() 268 | dns := match[2] 269 | if buf.Len() > 0 { 270 | buf.WriteString(",") 271 | } 272 | buf.WriteString("dns ") 273 | buf.WriteString(dns) 274 | domain_arr := strings.Split(strings.TrimSpace(regexp.MustCompile(`\s+`).ReplaceAllString(dns, " ")), " ") 275 | domain_match := func(s string) bool { 276 | for _, val := range domain_arr { 277 | for _, d := range strings.Split(s, " ") { 278 | if strings.HasSuffix(d, val) { 279 | return true 280 | } 281 | } 282 | } 283 | return false 284 | } 285 | re = regexp.MustCompile(`^\s*(\d+[\d.]+)\s+([^#]+)\s*(#.*)?$`) 286 | br := bufio.NewReader(fi) 287 | for { 288 | a, _, c := br.ReadLine() 289 | if c == io.EOF { 290 | break 291 | } 292 | s := string(a) 293 | match := re.FindStringSubmatch(s) 294 | if match != nil { 295 | if strings.Contains(match[3], "docker-connector:ignore") { 296 | continue 297 | } 298 | if !domain_match(match[2]) && !strings.Contains(match[3], "docker-connector:resolve") { 299 | continue 300 | } 301 | if buf.Len() > 0 { 302 | buf.WriteString(",") 303 | } 304 | buf.WriteString("host ") 305 | if match[1] == "127.0.0.1" { 306 | buf.WriteString(localIP.String()) 307 | } else { 308 | buf.WriteString(match[1]) 309 | } 310 | buf.WriteString(" ") 311 | buf.WriteString(match[2]) 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /desktop/expose.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | func packetIP(data []byte) net.IP { 11 | return net.IPv4(data[16], data[17], data[18], data[19]) 12 | } 13 | 14 | func handleExpose() { 15 | defer expose.Close() 16 | data := make([]byte, 2000) 17 | users := make(map[string]bool) 18 | for { 19 | n, addr, err := expose.ReadFromUDP(data) 20 | if err != nil { 21 | if strings.Contains(err.Error(), "closed") { 22 | logger.Info("expose server closed.") 23 | return 24 | } 25 | logger.Warningf("failed read udp msg, error: %v\n", err) 26 | continue 27 | } 28 | if users[addr.String()] { 29 | if pong { 30 | if data[0]&0xf0 == 0x40 { // IPv4 31 | total := 256*uint64(data[2]) + uint64(data[3]) // 总长度 32 | packet := data[:total] 33 | if packet[9] == 0x01 { // ICMPv4 34 | if packet[20] == 0x08 { // IPv4 echo request 35 | var echoReply bytes.Buffer 36 | echoReply.Write(packet[:12]) 37 | echoReply.Write(packet[16:20]) 38 | echoReply.Write(packet[12:16]) 39 | echoReply.WriteByte(0x00) 40 | echoReply.Write(packet[21:]) 41 | reply := echoReply.Bytes() 42 | reply[22] = 0x00 43 | reply[23] = 0x00 44 | crc := checkSum(reply[20:]) 45 | reply[22] = byte((crc & 0x00ff) >> 0) 46 | reply[23] = byte((crc & 0xff00) >> 8) 47 | logger.Debugf("Send IPv4 echo reply => %v %v\n", addr.String(), packetIP(packet)) 48 | expose.WriteToUDP(reply, addr) 49 | continue 50 | } else if packet[20] == 0x00 { 51 | logger.Debugf("Received IPv4 echo reply => %v %v\n", addr.String(), packetIP(packet)) 52 | } 53 | } 54 | } else if data[0]&0xf0 == 0x60 { // IPv6 55 | logger.Debugf("not supported") 56 | } 57 | } 58 | if _, err := conn.WriteToUDP(data[:n], cli); err != nil { 59 | logger.Warningf("udp write error: %v\n", err) 60 | } 61 | } else if data[0] == 1 { 62 | token := string(data[1:n]) 63 | clientIP := addr.String() 64 | logger.Debugf("client token => %s %s\n", clientIP, token) 65 | if ip, ok := tokens[token]; ok { 66 | users[clientIP] = true 67 | intIP := toIntIP(net.ParseIP(ip).To4(), 0, 1, 2, 3) 68 | logger.Infof("client session => %s %s %v\n", clientIP, ip, intIP) 69 | sessions[intIP] = addr 70 | var reply bytes.Buffer 71 | reply.WriteByte(1) 72 | // 验证成功返回IP 73 | ones, _ := subnet.Mask.Size() 74 | reply.WriteString(fmt.Sprintf("addr %s/%d", ip, ones)) 75 | reply.WriteString(fmt.Sprintf(",peer %s", localIP.String())) 76 | reply.WriteString(fmt.Sprintf(",mtu %d", MTU)) 77 | for k, v := range routes { 78 | if v { 79 | reply.WriteString(",route ") 80 | reply.WriteString(k) 81 | } 82 | } 83 | logger.Infof("reply client => %s %d %s %s\n", clientIP, reply.Len(), reply.String(), addr) 84 | expose.WriteToUDP(reply.Bytes(), addr) 85 | } else { 86 | logger.Infof("invalid token => %s %s\n", clientIP, token) 87 | } 88 | } else { 89 | expose.WriteToUDP([]byte{2}, addr) 90 | } 91 | } 92 | } 93 | 94 | func checkSum(data []byte) uint16 { 95 | var ( 96 | sum uint32 97 | length = len(data) 98 | index int 99 | ) 100 | //以每16位为单位进行求和,直到所有的字节全部求完或者只剩下一个8位字节(如果剩余一个8位字节说明字节数为奇数个) 101 | for length > 1 { 102 | sum += uint32(data[index]) + uint32(data[index+1])<<8 103 | index += 2 104 | length -= 2 105 | } 106 | //如果字节数为奇数个,要加上最后剩下的那个8位字节 107 | if length > 0 { 108 | sum += uint32(data[index]) 109 | } 110 | //加上高16位进位的部分 111 | sum += (sum >> 16) 112 | //别忘了返回的时候先求反 113 | return uint16(^sum) 114 | } 115 | -------------------------------------------------------------------------------- /desktop/go.mod: -------------------------------------------------------------------------------- 1 | module docker-connector 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.9 7 | github.com/kardianos/service v1.2.0 8 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 9 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 10 | golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /desktop/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 2 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 3 | github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= 4 | github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= 5 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 6 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 7 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= 8 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= 9 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= 10 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ= 13 | golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | -------------------------------------------------------------------------------- /desktop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/kardianos/service" 13 | "github.com/op/go-logging" 14 | ) 15 | 16 | var ( 17 | logger *logging.Logger 18 | conn *net.UDPConn 19 | cli *net.UDPAddr 20 | peer net.IP 21 | expose *net.UDPConn 22 | subnet *net.IPNet 23 | // TmpPeer peer tmp file 24 | TmpPeer = "" 25 | // MTU maximum transmission unit 26 | MTU = 1400 27 | host = "127.0.0.1" 28 | port = 2511 29 | addr = "192.168.251.1/24" 30 | configFile = "" 31 | watch = true 32 | routes = make(map[string]bool) 33 | tokens = make(map[string]string) 34 | iptables = make(map[string]bool) 35 | logLevel = "INFO" 36 | sessions = make(map[uint64]*net.UDPAddr) 37 | localIP = net.IP(make([]byte, 4)) 38 | pong = false 39 | cliAddr = "" 40 | bind = true 41 | logfile = "" 42 | leveledBackend logging.LeveledBackend 43 | hosts = "" 44 | ) 45 | 46 | func init() { 47 | TmpPeer = filepath.Join(os.TempDir(), "desktop-docker-connector.peer") 48 | logging.SetLevel(logging.INFO, "vpn") 49 | logger = logging.MustGetLogger("vpn") 50 | flag.IntVar(&MTU, "mtu", MTU, "mtu") 51 | flag.StringVar(&host, "host", host, "udp listen host") 52 | flag.IntVar(&port, "port", port, "udp listen port") 53 | flag.StringVar(&addr, "addr", addr, "virtual network address") 54 | flag.StringVar(&configFile, "config", configFile, "config file") 55 | flag.BoolVar(&watch, "watch", watch, "watch config file") 56 | flag.StringVar(&logLevel, "log-level", logLevel, "log level") 57 | flag.BoolVar(&pong, "pong", pong, "pong") 58 | flag.StringVar(&cliAddr, "cli", cliAddr, "udp client address") 59 | flag.BoolVar(&bind, "bind", bind, "bind to interface") 60 | flag.StringVar(&logfile, "log-file", logfile, "log file") 61 | } 62 | 63 | func runCmd(format string, a ...interface{}) error { 64 | args := fmt.Sprintf(format, a...) 65 | argv := strings.Split(args, " ") 66 | cmd := exec.Command(argv[0], argv[1:]...) 67 | logger.Infof("command => %s", args) 68 | return cmd.Run() 69 | } 70 | 71 | func runOutCmd(format string, a ...interface{}) (string, error) { 72 | args := fmt.Sprintf(format, a...) 73 | argv := strings.Split(args, " ") 74 | cmd := exec.Command(argv[0], argv[1:]...) 75 | out, err := cmd.CombinedOutput() 76 | logger.Infof("command => %s", args) 77 | if out != nil { 78 | outStr := string(out) 79 | return outStr, err 80 | } 81 | return "", err 82 | } 83 | 84 | func main() { 85 | flag.Parse() 86 | cfg := &service.Config{ 87 | Name: "DesktopDockerConnector", 88 | DisplayName: "Desktop Docker Connector", 89 | Description: "Connect Desktop and Docker", 90 | } 91 | if len(os.Args) > 1 { 92 | cfg.Arguments = os.Args[2:] 93 | } 94 | s, err := service.New(&Connector{}, cfg) 95 | if err != nil { 96 | logger.Fatal(err) 97 | } 98 | if len(os.Args) > 1 { 99 | switch os.Args[1] { 100 | case "install": 101 | if err := s.Install(); err != nil { 102 | logger.Fatal(err) 103 | } 104 | logger.Info("Install Service Success!") 105 | return 106 | case "uninstall": 107 | s.Stop() 108 | if err := s.Uninstall(); err != nil { 109 | logger.Fatal(err) 110 | } 111 | logger.Info("Uninstall Service Success!") 112 | return 113 | case "start": 114 | if err := s.Start(); err != nil { 115 | logger.Fatal(err) 116 | } 117 | logger.Info("Start Service Success!") 118 | return 119 | case "stop": 120 | if err := s.Stop(); err != nil { 121 | logger.Fatal(err) 122 | } 123 | logger.Info("Stop Service Success!") 124 | return 125 | case "restart": 126 | s.Stop() 127 | if err := s.Start(); err != nil { 128 | logger.Fatal(err) 129 | } 130 | logger.Info("Restart Service Success!") 131 | return 132 | case "config": 133 | sendConfig() 134 | return 135 | } 136 | } 137 | if err := s.Run(); err != nil { 138 | logger.Fatal(err) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /desktop/net_darwin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/songgao/water" 7 | ) 8 | 9 | func setup(local, peer net.IP, subnet *net.IPNet) *water.Interface { 10 | config := water.Config{ 11 | DeviceType: water.TUN, 12 | } 13 | iface, err := water.New(config) 14 | if err != nil { 15 | logger.Fatal(err) 16 | } 17 | logger.Infof("interface => %s\n", iface.Name()) 18 | if out, err := runOutCmd("%s %s inet %s %s netmask 255.255.255.255 up", "ifconfig", iface.Name(), local, peer); err != nil { 19 | logger.Warningf("%s\n", out) 20 | logger.Fatal(err) 21 | } 22 | runCmd("ifconfig %s mtu %d", iface.Name(), MTU) 23 | if err := runCmd("route -n add -host %s -interface %s", local, iface.Name()); err != nil { 24 | logger.Warning(err) 25 | } 26 | logger.Info("drawin setup done.") 27 | return iface 28 | } 29 | 30 | func addRoute(key string, peer net.IP) { 31 | if err := runCmd("route -n add -net %s %s", key, peer); err != nil { 32 | logger.Warning(err) 33 | } 34 | } 35 | 36 | func delRoute(key string) { 37 | runCmd("route -n delete -net %s", key) 38 | } 39 | -------------------------------------------------------------------------------- /desktop/net_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | "time" 8 | 9 | "github.com/songgao/water" 10 | ) 11 | 12 | func setup(local, peer net.IP, subnet *net.IPNet) *water.Interface { 13 | ones, _ := subnet.Mask.Size() 14 | mask := net.IP(subnet.Mask).String() 15 | addr := fmt.Sprintf("%s/%d", local, ones) 16 | config := water.Config{ 17 | DeviceType: water.TUN, 18 | PlatformSpecificParams: water.PlatformSpecificParams{ 19 | ComponentID: "tap0901", 20 | Network: addr, 21 | }, 22 | } 23 | iface, err := water.New(config) 24 | if err != nil { 25 | logger.Fatal(err) 26 | } 27 | if out, err := runOutCmd("netsh interface ip set address \"%s\" static %s %s %s", iface.Name(), local, mask, peer); err != nil { 28 | logger.Warningf("%s\n", out) 29 | logger.Fatal(err) 30 | } 31 | ipStr := local.String() 32 | for { 33 | if out, err := runOutCmd("netsh interface ip show addresses \"%s\"", iface.Name()); err == nil { 34 | if strings.Contains(out, ipStr) && strings.Contains(out, mask) { 35 | break 36 | } else { 37 | fmt.Println("waiting network setup...") 38 | time.Sleep(time.Second) 39 | } 40 | } else { 41 | break 42 | } 43 | } 44 | // netsh interface ipv4 show subinterfaces 45 | runCmd("netsh interface ipv4 set subinterface \"%s\" mtu=%d store=persistent", iface.Name(), MTU) 46 | runCmd("netsh interface ip delete dns \"%s\" all", iface.Name()) 47 | runCmd("netsh interface ip delete wins \"%s\" all", iface.Name()) 48 | return iface 49 | } 50 | 51 | func addRoute(key string, peer net.IP) { 52 | ip, subnet, err := net.ParseCIDR(key) 53 | if err != nil { 54 | return 55 | } 56 | if err := runCmd("route add %s mask %s %s", ip, net.IP(subnet.Mask).String(), peer); err != nil { 57 | logger.Warning(err) 58 | } 59 | } 60 | 61 | func delRoute(key string) { 62 | ip, subnet, err := net.ParseCIDR(key) 63 | if err != nil { 64 | return 65 | } 66 | runCmd("route delete %s mask %s %s", ip, net.IP(subnet.Mask).String(), peer) 67 | } 68 | -------------------------------------------------------------------------------- /desktop/options.conf.template: -------------------------------------------------------------------------------- 1 | # addr 192.168.251.1/24 2 | # mtu 1400 3 | # host 127.0.0.1 4 | # port 2511 5 | # route 172.100.0.0/16 6 | # route 172.18.0.0/16 7 | # route 172.100.0.0/16 8 | # expose 0.0.0.0:2512 9 | # token win10 192.168.251.3 10 | # token mac 192.168.251.4 11 | # iptables 172.63.79.0+172.21.81.0 12 | # iptables 172.21.81.0-172.63.79.0 13 | # hosts C:\Windows\System32\drivers\etc\hosts local 14 | # hosts /etc/hosts local 15 | # proxy 127.0.0.1:80 -------------------------------------------------------------------------------- /desktop/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | proxyServer *ProxyServer 12 | ) 13 | 14 | type ProxyServer struct { 15 | tcp map[string]net.Listener 16 | tmp map[string]byte 17 | } 18 | 19 | func NewProxyServer() *ProxyServer { 20 | return &ProxyServer{ 21 | tcp: make(map[string]net.Listener), 22 | } 23 | } 24 | 25 | func GetProxyServer() *ProxyServer { 26 | if proxyServer == nil { 27 | proxyServer = NewProxyServer() 28 | } 29 | return proxyServer 30 | } 31 | 32 | func (s *ProxyServer) StartClear() { 33 | s.tmp = make(map[string]byte) 34 | for k := range s.tcp { 35 | s.tmp[k] = 0 36 | } 37 | } 38 | 39 | func (s *ProxyServer) EndClear() { 40 | if s.tmp != nil { 41 | for k := range s.tmp { 42 | if s.tmp[k] == 0 { 43 | logger.Infof("close proxy: %s\n", k) 44 | s.tcp[k].Close() 45 | delete(s.tcp, k) 46 | } 47 | delete(s.tmp, k) 48 | } 49 | s.tmp = nil 50 | } 51 | } 52 | 53 | func (s *ProxyServer) Add(addr string) { 54 | argv := strings.Split(addr, ":") 55 | if len(argv) < 2 { 56 | argv = []string{"127.0.0.1", argv[0], argv[0]} 57 | } else if len(argv) < 3 { 58 | argv = []string{argv[0], argv[1], argv[1]} 59 | } 60 | addr = strings.Join(argv, ":") 61 | if _, ok := s.tcp[addr]; ok { 62 | return 63 | } 64 | s.tcp[addr] = nil 65 | } 66 | 67 | func (s *ProxyServer) Start(ip net.IP) { 68 | for key := range s.tcp { 69 | if svr, ok := s.tcp[key]; ok { 70 | if svr == nil { 71 | argv := strings.Split(key, ":") 72 | svr, err := net.Listen("tcp", fmt.Sprintf("%s:%s", ip.String(), argv[2])) 73 | if err != nil { 74 | logger.Warningf("proxy listen error: %s:%s %v\n", ip.String(), argv[2], err) 75 | return 76 | } 77 | logger.Infof("proxy listen %s:%s\n", ip.String(), argv[2]) 78 | s.tcp[key] = svr 79 | go func(addr string) { 80 | for { 81 | cli, err := svr.Accept() 82 | if err == nil { 83 | go s.serve(cli, addr) 84 | } else if strings.Contains(err.Error(), "closed") { 85 | break 86 | } else { 87 | logger.Warningf("proxy error %v\n", err) 88 | } 89 | } 90 | }(strings.Join(argv[:2], ":")) 91 | } 92 | } 93 | } 94 | } 95 | 96 | func (s *ProxyServer) serve(client net.Conn, addr string) { 97 | defer client.Close() 98 | remote, err := net.Dial("tcp", addr) 99 | if err != nil { 100 | return 101 | } 102 | defer remote.Close() 103 | go io.Copy(remote, client) 104 | io.Copy(client, remote) 105 | } 106 | -------------------------------------------------------------------------------- /desktop/service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | 14 | "github.com/fsnotify/fsnotify" 15 | "github.com/kardianos/service" 16 | "github.com/op/go-logging" 17 | "github.com/songgao/water" 18 | ) 19 | 20 | type Connector struct { 21 | iface *water.Interface 22 | stop bool 23 | } 24 | 25 | func (c *Connector) Start(s service.Service) error { 26 | c.stop = false 27 | go c.run() 28 | return nil 29 | } 30 | 31 | func (c *Connector) Stop(s service.Service) error { 32 | c.stop = true 33 | go func() { 34 | clearRoutes() 35 | if conn != nil { 36 | conn.Close() 37 | } 38 | if c.iface != nil { 39 | c.iface.Close() 40 | } 41 | }() 42 | return nil 43 | } 44 | 45 | func (c *Connector) run() { 46 | flag.Parse() 47 | if level, err := logging.LogLevel(logLevel); err == nil { 48 | logging.SetLevel(level, "vpn") 49 | } 50 | if logfile != "" { 51 | if !filepath.IsAbs(logfile) { 52 | path, err := filepath.Abs(os.Args[0]) 53 | if err == nil { 54 | logfile = filepath.Join(path, "..", logfile) 55 | } 56 | } 57 | file, err := os.OpenFile(logfile, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0660) 58 | if err == nil { 59 | backend := logging.NewLogBackend(file, "", log.LstdFlags) 60 | leveledBackend = logging.AddModuleLevel(backend) 61 | logger.SetBackend(leveledBackend) 62 | } 63 | } 64 | if configFile != "" && !filepath.IsAbs(configFile) { 65 | path, err := filepath.Abs(os.Args[0]) 66 | if err == nil { 67 | configFile = filepath.Join(path, "..", configFile) 68 | } 69 | logger.Infof("config file => %v\n", configFile) 70 | } 71 | var iface *water.Interface 72 | if _, err := os.Stat(configFile); err == nil { 73 | logger.Infof("load config(%v) => %s\n", watch, configFile) 74 | iface = loadConfig(iface, true) 75 | if watch { 76 | watcher, err := fsnotify.NewWatcher() 77 | if err != nil { 78 | logger.Fatal(err) 79 | } 80 | var timer *time.Timer 81 | defer watcher.Close() 82 | loader := func() { 83 | timer = nil 84 | loadConfig(iface, false) 85 | } 86 | go func() { 87 | for { 88 | select { 89 | case event, ok := <-watcher.Events: 90 | if !ok { 91 | return 92 | } 93 | if event.Op&fsnotify.Write == fsnotify.Write { 94 | logger.Debugf("config file changed => %s\n", configFile) 95 | if timer != nil { 96 | timer.Stop() 97 | } 98 | timer = time.AfterFunc(time.Duration(2)*time.Second, loader) 99 | } else if event.Op&fsnotify.Rename == fsnotify.Rename { 100 | logger.Debugf("config file renamed => %s\n", event.Name) 101 | if timer != nil { 102 | timer.Stop() 103 | } 104 | timer = time.AfterFunc(time.Duration(2)*time.Second, loader) 105 | if err = watcher.Remove(configFile); err != nil { 106 | logger.Warningf("remove watch error => %v\n", err) 107 | } 108 | if err = watcher.Add(event.Name); err != nil { 109 | logger.Warningf("watch error => %v\n", err) 110 | } 111 | } 112 | case err, ok := <-watcher.Errors: 113 | if !ok { 114 | return 115 | } 116 | logger.Info("error:", err) 117 | } 118 | } 119 | }() 120 | if err = watcher.Add(configFile); err == nil { 121 | if full, err := filepath.Abs(configFile); err != nil { 122 | logger.Debugf("watch config => %s\n", full) 123 | } else { 124 | logger.Debugf("watch config => %s\n", configFile) 125 | } 126 | } else { 127 | logger.Warningf("watch error => %v\n", err) 128 | } 129 | } 130 | } else { 131 | if peer, subnet, err = net.ParseCIDR(addr); err != nil { 132 | logger.Fatal(err) 133 | } 134 | copy([]byte(localIP), []byte(peer.To4())) 135 | localIP[3]++ 136 | if bind { 137 | iface = setup(localIP, peer, subnet) 138 | } 139 | } 140 | udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port)) 141 | if err != nil { 142 | logger.Fatalf("invalid address => %s:%d", host, port) 143 | } 144 | // 监听 145 | conn, err = net.ListenUDP("udp", udpAddr) 146 | if err != nil { 147 | logger.Fatalf("failed to listen %s:%d => %s", host, port, err.Error()) 148 | return 149 | } 150 | defer conn.Close() 151 | logger.Infof("listen => %v\n", conn.LocalAddr()) 152 | if cliAddr == "" { 153 | if tmp, err := ioutil.ReadFile(TmpPeer); err == nil { 154 | if cli, err = net.ResolveUDPAddr("udp", string(tmp)); err == nil { 155 | logger.Infof("load peer => %v\n", cli) 156 | } 157 | } 158 | } else { 159 | if cli, err = net.ResolveUDPAddr("udp", cliAddr); err == nil { 160 | logger.Infof("set peer => %v\n", cli) 161 | } 162 | } 163 | c.iface = iface 164 | go func() { 165 | if iface == nil { 166 | logger.Info("not bind to interface") 167 | return 168 | } 169 | buf := make([]byte, 2000) 170 | for { 171 | n, err := iface.Read(buf) 172 | if err != nil { 173 | if c.stop { 174 | break 175 | } 176 | logger.Warningf("tap read error: %v\n", err) 177 | continue 178 | } 179 | if localIP[0] == buf[16] && localIP[1] == buf[17] && localIP[2] == buf[18] && localIP[3] == buf[19] { 180 | if _, err := iface.Write(buf[:n]); err != nil { 181 | logger.Warningf("local write error: %v\n", err) 182 | } 183 | continue 184 | } 185 | if _, err := conn.WriteToUDP(buf[:n], cli); err != nil { 186 | if cli != nil { 187 | logger.Warningf("udp write error: %v\n", err) 188 | } 189 | continue 190 | } 191 | } 192 | }() 193 | var lastCli string 194 | var n int 195 | data := make([]byte, 2000) 196 | for { 197 | n, cli, err = conn.ReadFromUDP(data) 198 | if err != nil { 199 | if c.stop { 200 | break 201 | } 202 | logger.Warning("failed read udp msg, error: " + err.Error()) 203 | } 204 | dest := toIntIP(data, 16, 17, 18, 19) 205 | if sess, ok := sessions[dest]; ok && n > 1 { 206 | if _, err := expose.WriteToUDP(data[:n], sess); err != nil { 207 | logger.Warningf("session write error: %d %v %v\n", n, data[:n], err) 208 | } 209 | } else if bind { 210 | if _, err := iface.Write(data[:n]); err != nil { 211 | if data[0] == 0 && n == 1 { 212 | if lastCli == cli.String() { 213 | logger.Debugf("client heartbeat => %v\n", cli) 214 | } else { 215 | if lastCli == "" { 216 | logger.Infof("client init => %v\n", cli) 217 | } else { 218 | logger.Infof("client change => %v\n", cli) 219 | } 220 | lastCli = cli.String() 221 | if cliAddr == "" { 222 | ioutil.WriteFile(TmpPeer, []byte(lastCli), 0644) 223 | } 224 | sendControls(cli, iptables, hosts) 225 | } 226 | } else if data[0] == 1 && n > 1 { 227 | appendConfig(data[1:n]) 228 | } else { 229 | logger.Warningf("tun write error: %d %v %v\n", n, data[:n], err) 230 | } 231 | } 232 | } else if data[0] == 1 && n > 1 { 233 | appendConfig(data[1:n]) 234 | } 235 | } 236 | } 237 | 238 | func min(a int, b int) int { 239 | if a < b { 240 | return a 241 | } 242 | return b 243 | } 244 | 245 | func toIntIP(packet []byte, offset0 int, offset1 int, offset2 int, offset3 int) (sum uint64) { 246 | sum = 0 247 | sum += uint64(packet[offset0]) << 24 248 | sum += uint64(packet[offset1]) << 16 249 | sum += uint64(packet[offset2]) << 8 250 | sum += uint64(packet[offset3]) 251 | return sum 252 | } 253 | 254 | func sendControls(cli *net.UDPAddr, tables map[string]bool, hosts string) { 255 | logger.Infof("send controls => %v %v %v\n", cli, tables, hosts) 256 | var reply bytes.Buffer 257 | for k, v := range tables { 258 | if reply.Len() > 0 { 259 | reply.WriteString(",") 260 | } 261 | if v { 262 | reply.WriteString("connect ") 263 | } else { 264 | reply.WriteString("disconnect ") 265 | } 266 | reply.WriteString(k) 267 | } 268 | loadHosts(&reply, hosts) 269 | l := reply.Len() 270 | if l < 50 { 271 | logger.Infof("reply client => %s %d %s\n", cli, l, reply.String()) 272 | } else { 273 | logger.Infof("reply client => %s %d\n", cli, l) 274 | } 275 | if l > 0 { 276 | l16 := uint16(l) 277 | header := make([]byte, 3) 278 | header[0] = 1 279 | header[1] = byte(l16 >> 8) 280 | header[2] = byte(l16 & 0x00ff) 281 | if _, err := conn.WriteToUDP(header, cli); err != nil { 282 | logger.Warningf("write header error: %v %v\n", header, err) 283 | } 284 | tmp := reply.Bytes() 285 | for i := 0; i < l; i += MTU { 286 | if _, err := conn.WriteToUDP(tmp[i:min(i+MTU, l)], cli); err != nil { 287 | logger.Warningf("write body error: %v %v\n", l, err) 288 | } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /desktop/tools/install-service.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | %1 %2 3 | ver|find "5.">nul&&goto :Admin 4 | mshta vbscript:createobject("shell.application").shellexecute("%~s0","goto :Admin","","runas",1)(window.close)&goto :eof 5 | :Admin 6 | set path=%~dp0 7 | "%path%\docker-connector.exe" install -config options.conf -log-file connector.log 8 | pause -------------------------------------------------------------------------------- /desktop/tools/restart-service.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | %1 %2 3 | ver|find "5.">nul&&goto :Admin 4 | mshta vbscript:createobject("shell.application").shellexecute("%~s0","goto :Admin","","runas",1)(window.close)&goto :eof 5 | :Admin 6 | set path=%~dp0 7 | "%path%\docker-connector.exe" restart 8 | pause -------------------------------------------------------------------------------- /desktop/tools/start-connector.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | %1 %2 3 | ver|find "5.">nul&&goto :Admin 4 | mshta vbscript:createobject("shell.application").shellexecute("%~s0","goto :Admin","","runas",1)(window.close)&goto :eof 5 | :Admin 6 | set path=%~dp0 7 | "%path%\docker-connector.exe" -config options.conf 8 | pause -------------------------------------------------------------------------------- /desktop/tools/start-service.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | %1 %2 3 | ver|find "5.">nul&&goto :Admin 4 | mshta vbscript:createobject("shell.application").shellexecute("%~s0","goto :Admin","","runas",1)(window.close)&goto :eof 5 | :Admin 6 | set path=%~dp0 7 | "%path%\docker-connector.exe" start 8 | pause -------------------------------------------------------------------------------- /desktop/tools/stop-service.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | %1 %2 3 | ver|find "5.">nul&&goto :Admin 4 | mshta vbscript:createobject("shell.application").shellexecute("%~s0","goto :Admin","","runas",1)(window.close)&goto :eof 5 | :Admin 6 | set path=%~dp0 7 | "%path%\docker-connector.exe" stop 8 | pause -------------------------------------------------------------------------------- /desktop/tools/uninstall-service.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | %1 %2 3 | ver|find "5.">nul&&goto :Admin 4 | mshta vbscript:createobject("shell.application").shellexecute("%~s0","goto :Admin","","runas",1)(window.close)&goto :eof 5 | :Admin 6 | set path=%~dp0 7 | "%path%\docker-connector.exe" uninstall 8 | pause 9 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.13.5-alpine3.10 AS builder 2 | ARG TARGETPLATFORM 3 | ARG BUILDPLATFORM 4 | WORKDIR /build 5 | ENV GOPROXY https://goproxy.cn 6 | ADD . /build/ 7 | RUN CGO_ENABLED=0 GOARCH=${TARGETPLATFORM:6:5} GOOS=linux go build -ldflags "-s -w" -tags netgo -o desktop-connector . 8 | 9 | FROM alpine:3.10 10 | RUN apk add --no-cache iptables && rm -rf /var/cache/apk/* 11 | COPY --from=builder /build/desktop-connector /usr/bin/ 12 | CMD [ "desktop-connector" ] -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # desktop-connector 2 | 3 | Connect to `docker-connector` in `macOS`/`Windows` host and route the request from `macOS`/`Windows` host to the container correctly. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | $ docker run -it -d --net host --cap-add NET_ADMIN --restart always --name desktop-connector wenjunxiao/desktop-docker-connector 9 | ``` 10 | 11 | ## Compile 12 | 13 | ### Docker 14 | 15 | ```bash 16 | $ docker build -t desktop-docker-connector . 17 | ``` 18 | 19 | ### Local 20 | Local compile 21 | ```bash 22 | $ GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -tags netgo -o desktop-connector . 23 | ``` 24 | 25 | ## Dev 26 | 27 | ### Centos 28 | Build dev docker image 29 | ```bash 30 | $ docker run -it --name centos-go centos:7 bash 31 | > yum install -y epel-release 32 | > yum install go net-tools tcpdump initscripts 33 | > go env -w GOPROXY=https://goproxy.cn,direct 34 | $ docker commit centos-go centos-go 35 | ``` 36 | 37 | Run with the dev image 38 | ```bash 39 | $ docker run -it --cap-add NET_ADMIN -v $PWD:/workspace --workdir /workspace centos-go bash 40 | > # go mod init main 41 | > go env -w GOPROXY=https://goproxy.cn,direct 42 | > go run . 43 | ``` 44 | Or build 45 | ```bash 46 | $ go build -ldflags "-s -w" -tags netgo -o desktop-connector . 47 | ``` 48 | 49 | ### Alpine 50 | 51 | ```bash 52 | $ docker run -it --cap-add NET_ADMIN --net host -v $PWD:/workspace --workdir /workspace golang:1.13.5-alpine3.10 bash 53 | > go env -w GOPROXY=https://goproxy.cn,direct 54 | > go run . 55 | ``` 56 | To install other tools, such as `iptables`, replace apk repository source to avalibale one, such as in china 57 | ```bash 58 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 59 | ``` 60 | 61 | ## Multi-Platform 62 | 63 | Build a multi-platform using [buildx](https://docs.docker.com/buildx/working-with-buildx/). 64 | First, must create target platforms build env and use it. 65 | ```bash 66 | $ docker buildx create --platform linux/amd64,linux/arm64/v8 --use 67 | ``` 68 | 69 | ### Push 70 | 71 | Build and push to hub directly. 72 | ```bash 73 | $ docker buildx build --platform linux/amd64,linux/arm64/v8 -t wenjunxiao/desktop-docker-connector:latest . --push 74 | ``` 75 | 76 | ### Local 77 | In the local development environment, you can use `--load` to save to local mirror, 78 | but only a single platform can be used at a time. 79 | 80 | Build an example for platform `linux/arm64`, that only prints platform. 81 | ```bash 82 | $ cat < main.go \ 94 | && echo " fmt.Println(\"build on \$BUILDPLATFORM, build for \$TARGETPLATFORM\")" >> main.go \ 95 | && echo "}" >> main.go 96 | RUN cat main.go && CGO_ENABLED=0 GOARCH=\${TARGETPLATFORM:6} GOOS=linux go build -a -o demo . 97 | 98 | FROM alpine:3.10 99 | COPY --from=builder /build/ /usr/bin/ 100 | EOF 101 | ``` 102 | Run with image `demo:arm64` 103 | ```bash 104 | $ docker run --rm -it demo:arm64 sh 105 | WARNING: The requested image's platform (linux/arm64) does not match the detected host platform (linux/amd64) and no specific platform was requested 106 | / # demo 107 | runtime GOARCH: arm64 108 | build on linux/amd64, build for linux/arm64 109 | ``` 110 | Run with image `demo:arm64` and platform `linux/arm64` 111 | ```bash 112 | $ docker run --rm -it --platform linux/arm64 demo:arm64 sh 113 | / # demo 114 | runtime GOARCH: arm64 115 | build on linux/amd64, build for linux/arm64 116 | ``` 117 | 118 | ## Advance 119 | 120 | Advanced feature depends on `iptables` tool. But all rules of `iptables` will be cleared after restart, so the connector will set all the rules from [desktop](../desktop) after restart. 121 | 122 | ### Connect Two Subnet 123 | Two sub bridge net created by follow command 124 | ```bash 125 | $ docker network create --subnet 172.18.0.0/16 net1 126 | $ docker network create --subnet 172.19.0.0/16 net2 127 | ``` 128 | Start two containers using the above IPs respectively, and then ping each other after both started. 129 | ```bash 130 | $ docker run --rm -it -e "PS1=`ifconfig eth0 | sed -nr 's/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p'`> " --net static alpine sh 131 | 172.18.0.3> ping -c 1 -W 1 172.19.0.2 132 | PING 172.19.0.2 (172.19.0.2): 56 data bytes 133 | 134 | --- 172.19.0.2 ping statistics --- 135 | 1 packets transmitted, 0 packets received, 100% packet loss 136 | ``` 137 | 138 | ```bash 139 | $ docker run --rm -it -e "PS1=`ifconfig eth0 | sed -nr 's/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p'`> " --net test alpine sh 140 | 172.19.0.2> ping -c 1 -W 1 172.18.0.3 141 | PING 172.18.0.3 (172.18.0.3): 56 data bytes 142 | 143 | --- 172.18.0.3 ping statistics --- 144 | 1 packets transmitted, 0 packets received, 100% packet loss 145 | ``` 146 | 147 | If you want the two containers to be able to access each other, you need to enter the connector container and execute the following command 148 | ```bash 149 | $ docker exec -it desktop-connector sh 150 | $ docker exec -it desktop-connector sh 151 | > route -n 152 | Kernel IP routing table 153 | Destination Gateway Genmask Flags Metric Ref Use Iface 154 | 0.0.0.0 192.168.65.1 0.0.0.0 UG 0 0 0 eth0 155 | 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo 156 | 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 157 | 172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-bde9105adf96 158 | 172.19.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-b0bc2fea23d4 159 | 192.168.65.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 160 | 192.168.65.5 0.0.0.0 255.255.255.255 UH 0 0 0 services1 161 | ``` 162 | The iface of subnet `172.18.0.0/16` is `br-bde9105adf96`, and the iface of subnet `172.19.0.0/16` is `br-b0bc2fea23d4`. 163 | Connect them by following commands in connector container 164 | ```bash 165 | $ iptables -I DOCKER-USER -i br-bde9105adf96 -o br-b0bc2fea23d4 -j ACCEPT 166 | $ iptables -I DOCKER-USER -i br-b0bc2fea23d4 -o br-bde9105adf96 -j ACCEPT 167 | ``` 168 | Then, ping the two subnet again. 169 | ```bash 170 | 172.18.0.3> ping -c 1 -W 1 172.19.0.2 171 | PING 172.19.0.2 (172.19.0.2): 56 data bytes 172 | 64 bytes from 172.19.0.2: seq=0 ttl=63 time=0.227 ms 173 | 174 | --- 172.19.0.2 ping statistics --- 175 | 1 packets transmitted, 1 packets received, 0% packet loss 176 | round-trip min/avg/max = 0.227/0.227/0.227 ms 177 | ``` 178 | ```bash 179 | 172.19.0.2> ping -c 1 -W 1 172.18.0.3 180 | PING 172.18.0.3 (172.18.0.3): 56 data bytes 181 | 64 bytes from 172.18.0.3: seq=0 ttl=63 time=0.187 ms 182 | 183 | --- 172.18.0.3 ping statistics --- 184 | 1 packets transmitted, 1 packets received, 0% packet loss 185 | round-trip min/avg/max = 0.187/0.187/0.187 ms 186 | ``` 187 | 188 | ### DNS Resolve 189 | 190 | The connector container provider a simple dns server, which only resolve some special domain. 191 | Route the dns request to connector 192 | ```bash 193 | $ iptables -t nat -I PREROUTING -p udp --dport 53 -m string --algo bm --string local -j DNAT --to-destination 192.168.251.1 194 | ``` 195 | Show the rules 196 | ```bash 197 | $ iptables -t nat -L PREROUTING -vn --line-number 198 | ``` 199 | Delete the rule 200 | ```bash 201 | $ iptables -t nat -D PREROUTING 202 | ``` 203 | 204 | ### Multi-Subnet 205 | 206 | If a container has more than one subnet, which is added by `docker network connect`, you need to specify `rt_table` for each subnet (Please install `iproute2` in you container). 207 | ```bash 208 | $ ip rule add from table prio 1 209 | $ ip route add default via dev table 210 | ``` 211 | For example, add two `rt_table` 212 | ```bash 213 | $ echo "201 rt1" >> /etc/iproute2/rt_tables 214 | $ echo "202 rt2" >> /etc/iproute2/rt_tables 215 | ``` 216 | and then specify `rt_table` for two subnet `172.100.0.1/16` and `172.17.0.1/16` 217 | ```bash 218 | # specify for 172.100.0.1/16 219 | $ ip route add default via 172.100.0.1 dev `ip addr show | grep 172.100.0 | awk '{print $NF}'` table rt1 220 | $ ip rule add from `ip addr show | grep 172.100.0 | awk '{print $2}' | awk -F/ '{print $1}'` table rt1 prio 1 221 | # specify for 172.17.0.0/16 222 | $ ip route add default via 172.17.0.1 dev `ip addr show | grep 172.17.0 | awk '{print $NF}'` table rt2 223 | $ ip rule add from `ip addr show | grep 172.17.0 | awk '{print $2}' | awk -F/ '{print $1}'` table rt2 prio 1 224 | ``` 225 | 226 | -------------------------------------------------------------------------------- /docker/dns_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "regexp" 10 | "strings" 11 | 12 | "golang.org/x/net/dns/dnsmessage" 13 | ) 14 | 15 | type DNSServer struct { 16 | udp *net.UDPConn 17 | a map[string][4]byte 18 | ptr map[string]string 19 | tmp map[string]byte 20 | up *net.UDPConn 21 | pending map[string][]*net.UDPAddr 22 | } 23 | 24 | func NewDnsServer() *DNSServer { 25 | return &DNSServer{ 26 | a: make(map[string][4]byte), 27 | ptr: make(map[string]string), 28 | } 29 | } 30 | 31 | func (s *DNSServer) Start(ip net.IP) error { 32 | if s.udp != nil { 33 | return nil 34 | } 35 | go s.run(ip) 36 | return nil 37 | } 38 | 39 | func (s *DNSServer) run(ip net.IP) { 40 | conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip, Port: 53}) 41 | if err != nil { 42 | panic(err) 43 | } 44 | fmt.Printf("dns listen => %v\n", conn.LocalAddr()) 45 | defer conn.Close() 46 | s.udp = conn 47 | for { 48 | buf := make([]byte, 512) 49 | _, addr, _ := conn.ReadFromUDP(buf) 50 | var msg dnsmessage.Message 51 | if err := msg.Unpack(buf); err != nil { 52 | fmt.Println(err) 53 | continue 54 | } 55 | go s.serve(addr, conn, msg) 56 | } 57 | } 58 | 59 | func (s *DNSServer) Add(record string) { 60 | argv := strings.Split(record, " ") 61 | if len(argv) < 2 { 62 | return 63 | } 64 | ipv4 := net.ParseIP(argv[0]).To4() 65 | for i := 1; i < len(argv); i++ { 66 | s.a[argv[i]+"."] = [4]byte{ipv4[0], ipv4[1], ipv4[2], ipv4[3]} 67 | if s.tmp != nil { 68 | s.tmp[argv[i]+"."] = 1 69 | } 70 | } 71 | s.ptr[argv[0]+".in-addr.arpa."] = argv[1] + "." 72 | if s.tmp != nil { 73 | s.tmp[argv[0]+".in-addr.arpa."] = 1 74 | } 75 | } 76 | 77 | func (s *DNSServer) StartClear() { 78 | s.tmp = make(map[string]byte) 79 | for k := range s.a { 80 | s.tmp[k] = 0 81 | } 82 | for k := range s.ptr { 83 | s.tmp[k] = 0 84 | } 85 | } 86 | 87 | func (s *DNSServer) EndClear() { 88 | if s.tmp != nil { 89 | for k := range s.tmp { 90 | if s.tmp[k] == 0 { 91 | delete(s.a, k) 92 | delete(s.ptr, k) 93 | } 94 | delete(s.tmp, k) 95 | } 96 | s.tmp = nil 97 | } 98 | } 99 | 100 | func (s *DNSServer) serve(addr *net.UDPAddr, conn *net.UDPConn, msg dnsmessage.Message) { 101 | if len(msg.Questions) < 1 { 102 | return 103 | } 104 | question := msg.Questions[0] 105 | var ( 106 | queryTypeStr = question.Type.String() 107 | queryNameStr = question.Name.String() 108 | queryType = question.Type 109 | queryName, _ = dnsmessage.NewName(queryNameStr) 110 | ) 111 | var resource dnsmessage.Resource 112 | switch queryType { 113 | case dnsmessage.TypeAAAA: 114 | fallthrough 115 | case dnsmessage.TypeA: 116 | if rst, ok := s.a[queryNameStr]; ok { 117 | resource = newAResource(queryName, rst) 118 | } else { 119 | fmt.Printf("not fount A record queryName: [%s] \n", queryNameStr) 120 | s.redirect(addr, queryName.String(), msg) 121 | return 122 | } 123 | case dnsmessage.TypePTR: 124 | if rst, ok := s.ptr[queryName.String()]; ok { 125 | resource = newPTRResource(queryName, rst) 126 | } else { 127 | fmt.Printf("not fount PTR record queryName: [%s] \n", queryNameStr) 128 | s.redirect(addr, queryName.String(), msg) 129 | return 130 | } 131 | default: 132 | fmt.Printf("not support dns queryType: [%s] \n", queryTypeStr) 133 | return 134 | } 135 | msg.Response = true 136 | msg.Answers = append(msg.Answers, resource) 137 | s.response(addr, msg) 138 | } 139 | 140 | func readNameServer() string { 141 | fi, err := os.Open("/etc/resolv.conf") 142 | if err != nil { 143 | fmt.Printf("Error: %s\n", err) 144 | return "" 145 | } 146 | defer fi.Close() 147 | re := regexp.MustCompile(`^\s*nameserver\s+(\d+[\d.]+)`) 148 | br := bufio.NewReader(fi) 149 | for { 150 | a, _, c := br.ReadLine() 151 | if c == io.EOF { 152 | break 153 | } 154 | match := re.FindStringSubmatch(string(a)) 155 | if match != nil { 156 | return match[1] 157 | } 158 | } 159 | return "" 160 | } 161 | 162 | func (s *DNSServer) redirect(addr *net.UDPAddr, name string, msg dnsmessage.Message) { 163 | packed, err := msg.Pack() 164 | if err != nil { 165 | fmt.Println(err) 166 | return 167 | } 168 | if s.up == nil { 169 | ip := readNameServer() 170 | if ip == "" { 171 | s.response(addr, msg) 172 | return 173 | } 174 | fmt.Printf("dns upstream is %v\n", ip) 175 | s.up, err = net.DialUDP("udp", nil, &net.UDPAddr{IP: net.ParseIP(ip), Port: 53}) 176 | if err != nil { 177 | s.response(addr, msg) 178 | return 179 | } 180 | s.pending = make(map[string][]*net.UDPAddr) 181 | go s.upstream(s.up) 182 | } 183 | s.pending[name] = append(s.pending[name], addr) 184 | if _, err := s.up.Write(packed); err != nil { 185 | fmt.Println(err) 186 | s.up = nil 187 | return 188 | } 189 | } 190 | 191 | func (s *DNSServer) upstream(up *net.UDPConn) { 192 | buf := make([]byte, 512) 193 | for { 194 | _, err := up.Read(buf) 195 | if err != nil { 196 | fmt.Println("read upstream msg, error: " + err.Error()) 197 | break 198 | } 199 | var rsp dnsmessage.Message 200 | if err := rsp.Unpack(buf); err != nil { 201 | fmt.Println(err) 202 | continue 203 | } 204 | if len(rsp.Questions) < 1 { 205 | continue 206 | } 207 | question := rsp.Questions[0] 208 | name := question.Name.String() 209 | addrs := s.pending[name] 210 | delete(s.pending, name) 211 | for _, addr := range addrs { 212 | s.response(addr, rsp) 213 | } 214 | } 215 | } 216 | 217 | func (s *DNSServer) response(addr *net.UDPAddr, msg dnsmessage.Message) { 218 | packed, err := msg.Pack() 219 | if err != nil { 220 | fmt.Println(err) 221 | return 222 | } 223 | if _, err := s.udp.WriteToUDP(packed, addr); err != nil { 224 | fmt.Println(err) 225 | } 226 | } 227 | 228 | func newAResource(query dnsmessage.Name, a [4]byte) dnsmessage.Resource { 229 | return dnsmessage.Resource{ 230 | Header: dnsmessage.ResourceHeader{ 231 | Name: query, 232 | Class: dnsmessage.ClassINET, 233 | TTL: 600, 234 | }, 235 | Body: &dnsmessage.AResource{ 236 | A: a, 237 | }, 238 | } 239 | } 240 | 241 | func newPTRResource(query dnsmessage.Name, ptr string) dnsmessage.Resource { 242 | name, _ := dnsmessage.NewName(ptr) 243 | return dnsmessage.Resource{ 244 | Header: dnsmessage.ResourceHeader{ 245 | Name: query, 246 | Class: dnsmessage.ClassINET, 247 | }, 248 | Body: &dnsmessage.PTRResource{ 249 | PTR: name, 250 | }, 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /docker/go.mod: -------------------------------------------------------------------------------- 1 | module desktop-connector 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 7 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd 8 | ) 9 | -------------------------------------------------------------------------------- /docker/go.sum: -------------------------------------------------------------------------------- 1 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= 2 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= 3 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= 4 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 5 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 6 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 8 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 9 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 10 | -------------------------------------------------------------------------------- /docker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "time" 12 | 13 | "github.com/songgao/water" 14 | ) 15 | 16 | var ( 17 | // MTU maximum transmission unit 18 | MTU = 1400 19 | debug = false 20 | host = "host.docker.internal" 21 | port = 2511 22 | addr = "192.168.251.1/24" 23 | heartbeat = 5000 24 | chain = "DOCKER-USER" 25 | dnsSvr *DNSServer 26 | ) 27 | 28 | func init() { 29 | flag.BoolVar(&debug, "debug", debug, "Provide debug info") 30 | flag.IntVar(&MTU, "mtu", MTU, "network MTU") 31 | flag.StringVar(&host, "host", host, "host to connect") 32 | flag.IntVar(&port, "port", port, "port to connect") 33 | flag.StringVar(&addr, "addr", addr, "virtual network address") 34 | flag.StringVar(&chain, "chain", chain, "iptables chain name") 35 | flag.IntVar(&heartbeat, "heartbeat", heartbeat, "heartbeat") 36 | } 37 | 38 | func runCmd(args string) string { 39 | argv := strings.Split(args, " ") 40 | cmd := exec.Command(argv[0], argv[1:]...) 41 | out, err := cmd.CombinedOutput() 42 | if err == nil { 43 | return string(out) 44 | } 45 | fmt.Printf("command error => %s %v\n", args, err) 46 | return "" 47 | } 48 | 49 | func getRoutes() map[string]string { 50 | routes := make(map[string]string) 51 | lines := strings.Split(runCmd("route -n"), "\n") 52 | for _, line := range lines { 53 | fields := strings.Fields(line) 54 | if len(fields) > 2 { 55 | routes[fields[0]] = fields[len(fields)-1] 56 | } 57 | } 58 | return routes 59 | } 60 | 61 | func iptables(a, i, o string) error { 62 | // iptables -I DOCKER-USER -i br-net1 -o br-net2 -j ACCEPT 63 | // iptables -I DOCKER-USER -i br-net2 -o br-net1 -j ACCEPT 64 | fmt.Printf("iptables %s %s -i %s -o %s\n", a, chain, i, o) 65 | cmd := exec.Command("iptables", a, chain, "-i", i, "-o", o, "-j", "ACCEPT") 66 | return cmd.Run() 67 | } 68 | 69 | // domain dot `.` in dns query is replaced by the length of next domain part 70 | // such as `www.example.com` is converted to `www\x07example\x03com` 71 | // use iptable hex-string, the content is `www|07|example|03|com|` 72 | func hexDomain(s string) string { 73 | vals := strings.Split(s, ".") 74 | if len(vals) == 1 { 75 | return s 76 | } 77 | var buf bytes.Buffer 78 | if len(vals[0]) > 0 { 79 | buf.WriteString(vals[0]) 80 | } 81 | buf.WriteString("|") 82 | for i := 1; i < len(vals); i++ { 83 | buf.WriteString(fmt.Sprintf("%02x", len(vals[i]))) 84 | buf.WriteString("|") 85 | buf.WriteString(vals[i]) 86 | buf.WriteString("|") 87 | } 88 | return buf.String() 89 | } 90 | 91 | // rediect the dns query of the domain to specified ip 92 | // iptables -t nat -L PREROUTING -vn --line-number 93 | func rediectDns(domains []string, ip net.IP) error { 94 | for _, domain := range domains { 95 | act := "-I" 96 | if domain[0] == '-' { 97 | domain = domain[1:] 98 | act = "-D" 99 | } else { 100 | argv := []string{"iptables", "-t", "nat", "-D", "PREROUTING", "-p", "udp", "--dport", "53", 101 | "-m", "string", "--algo", "bm", "--hex-string", hexDomain(domain), "-j", "DNAT", "--to-destination", ip.String(), 102 | } 103 | fmt.Printf("clear dns => %v %v\n", strings.Join(argv, " "), exec.Command(argv[0], argv[1:]...).Run()) 104 | } 105 | argv := []string{"iptables", "-t", "nat", act, "PREROUTING", "-p", "udp", "--dport", "53", 106 | "-m", "string", "--algo", "bm", "--hex-string", hexDomain(domain), "-j", "DNAT", "--to-destination", ip.String(), 107 | } 108 | fmt.Printf("dns command => %s %v\n", strings.Join(argv, " "), exec.Command(argv[0], argv[1:]...).Run()) 109 | } 110 | return nil 111 | } 112 | 113 | func applyControls(cmds []string, ip net.IP) { 114 | routes := getRoutes() 115 | if dnsSvr != nil { 116 | dnsSvr.StartClear() 117 | } 118 | for _, val := range cmds { 119 | vals := strings.Split(val, " ") 120 | fmt.Printf("control => %s\n", val) 121 | switch vals[0] { 122 | case "connect": 123 | i1 := routes[vals[1]] 124 | i2 := routes[vals[2]] 125 | if len(i1) > 0 && len(i2) > 0 { 126 | if iptables("-C", i1, i2) != nil { 127 | iptables("-I", i1, i2) 128 | iptables("-I", i2, i1) 129 | } 130 | } 131 | case "disconnect": 132 | i1 := routes[vals[1]] 133 | i2 := routes[vals[2]] 134 | if len(i1) > 0 && len(i2) > 0 { 135 | iptables("-D", i1, i2) 136 | iptables("-D", i2, i1) 137 | } 138 | case "dns": 139 | rediectDns(vals[1:], ip) 140 | case "host": 141 | if dnsSvr == nil { 142 | dnsSvr = NewDnsServer() 143 | } 144 | dnsSvr.Add(strings.Join(vals[1:], " ")) 145 | } 146 | } 147 | if dnsSvr != nil { 148 | dnsSvr.EndClear() 149 | dnsSvr.Start(ip) 150 | } 151 | } 152 | 153 | func main() { 154 | flag.Parse() 155 | if _, err := os.Stat("/dev/net"); err != nil { 156 | os.Mkdir("/dev/net", os.ModePerm) 157 | cmd := exec.Command("mknod", "/dev/net/tun", "c", "10", "200") 158 | err = cmd.Run() 159 | if err != nil { 160 | fmt.Println(err) 161 | os.Exit(1) 162 | } 163 | } else if _, err := os.Stat("/dev/net/tun"); err != nil { 164 | cmd := exec.Command("mknod", "/dev/net/tun", "c", "10", "200") 165 | err = cmd.Run() 166 | if err != nil { 167 | fmt.Println(err) 168 | os.Exit(1) 169 | } 170 | } 171 | config := water.Config{ 172 | DeviceType: water.TUN, 173 | } 174 | iface, err := water.New(config) 175 | if err != nil { 176 | fmt.Println(err) 177 | os.Exit(1) 178 | } 179 | fmt.Printf("interface => %v\n", iface.Name()) 180 | args := fmt.Sprintf("%s link set dev %s up mtu %d qlen 100", "ip", iface.Name(), MTU) 181 | argv := strings.Split(args, " ") 182 | cmd := exec.Command(argv[0], argv[1:]...) 183 | err = cmd.Run() 184 | if err != nil { 185 | fmt.Println(err) 186 | os.Exit(1) 187 | } 188 | ip, subnet, _ := net.ParseCIDR(addr) 189 | peer := net.IP(make([]byte, 4)) 190 | copy([]byte(peer), []byte(ip.To4())) 191 | peer[3]++ 192 | args = fmt.Sprintf("%s addr add dev %s local %s peer %s", "ip", iface.Name(), ip, peer) 193 | fmt.Printf("command => %s\n", args) 194 | argv = strings.Split(args, " ") 195 | cmd = exec.Command(argv[0], argv[1:]...) 196 | err = cmd.Run() 197 | if err != nil { 198 | fmt.Println(err) 199 | os.Exit(1) 200 | } 201 | args = fmt.Sprintf("%s route add %s via %s dev %s", "ip", subnet, peer, iface.Name()) 202 | fmt.Printf("command => %s\n", args) 203 | argv = strings.Split(args, " ") 204 | cmd = exec.Command(argv[0], argv[1:]...) 205 | err = cmd.Run() 206 | if err != nil { 207 | fmt.Printf("invalid command => %s\n", args) 208 | os.Exit(1) 209 | } 210 | udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port)) 211 | if err != nil { 212 | fmt.Printf("invalid address => %s:%d\n", host, port) 213 | os.Exit(1) 214 | } 215 | conn, err := net.DialUDP("udp", nil, udpAddr) 216 | if err != nil { 217 | fmt.Printf("failed to dial %s:%d => %s\n", host, port, err.Error()) 218 | os.Exit(1) 219 | } 220 | defer conn.Close() 221 | fmt.Printf("local => %s\n", conn.LocalAddr()) 222 | fmt.Printf("remote => %s\n", conn.RemoteAddr()) 223 | conn.Write([]byte{0}) 224 | requested := make(chan bool, 1) 225 | go func() { 226 | buf := make([]byte, 2000) 227 | for { 228 | n, err := iface.Read(buf) 229 | if err != nil { 230 | fmt.Printf("tun read error: %v\n", err) 231 | continue 232 | } 233 | if _, err := conn.Write(buf[:n]); err != nil { 234 | fmt.Printf("udp write error: %v\n", err) 235 | } 236 | requested <- true 237 | } 238 | }() 239 | go func() { 240 | duration := time.Duration(time.Millisecond * time.Duration(heartbeat)) 241 | for { 242 | select { 243 | case <-requested: 244 | continue 245 | case <-time.After(duration): 246 | conn.Write([]byte{0}) 247 | } 248 | } 249 | }() 250 | data := make([]byte, 2000) 251 | for { 252 | n, err := conn.Read(data) 253 | if err != nil { 254 | fmt.Println("failed read udp msg, error: " + err.Error()) 255 | } 256 | if _, err := iface.Write(data[:n]); err != nil { 257 | if data[0] == 1 { 258 | var l int = 0 259 | l += int(data[1]) << 8 260 | l += int(data[2]) 261 | var buf = make([]byte, l) 262 | var pos = n - 3 263 | copy(buf, data[3:n]) 264 | for pos < l { 265 | if n, err = conn.Read(data); err != nil { 266 | fmt.Println("failed read udp msg, error: " + err.Error()) 267 | break 268 | } 269 | copy(buf[pos:], data[:n]) 270 | pos += n 271 | } 272 | if l > 0 { 273 | applyControls(strings.Split(string(buf), ","), ip) 274 | } 275 | } else { 276 | fmt.Printf("tun write error: %v\n", err) 277 | } 278 | } 279 | requested <- true 280 | } 281 | } 282 | --------------------------------------------------------------------------------