├── .gitignore ├── Dockerfile ├── Readme.md ├── docker-entrypoint.sh ├── etc ├── ssh │ └── sshd_config └── supervisord.conf └── vpn.sh /.gitignore: -------------------------------------------------------------------------------- 1 | docker-compose.yml 2 | .env 3 | ignore/ 4 | .idea/ 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21.2 2 | 3 | RUN apk add --no-cache \ 4 | --repository https://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ 5 | openconnect \ 6 | && apk add --no-cache openvpn openssh \ 7 | && apk add --no-cache py3-pip \ 8 | && apk add --no-cache bind-tools curl \ 9 | && apk add --no-cache supervisor 10 | RUN pip install --break-system-packages pproxy[accelerated] 11 | 12 | # Fix Cannot open "/proc/sys/net/ipv4/route/flush": Read-only file system 13 | # See https://serverfault.com/questions/878443/when-running-vpnc-in-docker-get-cannot-open-proc-sys-net-ipv4-route-flush 14 | RUN rm -f /etc/vpnc/vpnc-script \ 15 | && wget https://gitlab.com/openconnect/vpnc-scripts/-/raw/master/vpnc-script -O /etc/vpnc/vpnc-script \ 16 | && chmod +x /etc/vpnc/vpnc-script 17 | 18 | # create the root user's .ssh directory 19 | # unlock the root account 20 | RUN mkdir /root/.ssh \ 21 | && chmod 0700 /root/.ssh \ 22 | && sed -i 's/^root:!::0:::::/root:::0:::::/' /etc/shadow 23 | 24 | COPY docker-entrypoint.sh / 25 | COPY etc/ssh/sshd_config /etc/ssh/ 26 | COPY etc/supervisord.conf /etc/ 27 | 28 | ENTRYPOINT ["/docker-entrypoint.sh"] 29 | 30 | VOLUME /etc/ssh/ 31 | EXPOSE 22 32 | EXPOSE 1080 33 | EXPOSE 1088 34 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | docker-vpn is an alternative to installing VPN software on your host system and routing all your traffic through a VPN. This is useful if you want to have control over which traffic is sent through the VPN. Sending all your traffic through a VPN is a privacy concern and limits your internet connection to the speed of your VPN. 4 | 5 | The [`ethack/vpn`](https://hub.docker.com/r/ethack/vpn) Docker image and accompanying shell script provide the following: 6 | - OpenVPN client 7 | - Cisco AnyConnect or Juniper Pulse client 8 | - SSH server (default port 2222) with public key authentication enabled and configured 9 | - SOCKS 5 server (default port 1080) 10 | - HTTP Proxy server (default port 1088) 11 | - SSH config file entry created for each VPN connection 12 | 13 | ## Install 14 | 15 | - [Install Docker](https://docs.docker.com/install/) using the instructions or use `curl -fsSL https://get.docker.com -o get-docker.sh | sh` if you have a supported linux distro and like to live dangerously. 16 | - Source `vpn.sh` in your `.bashrc` file or current shell. E.g. `source vpn.sh` 17 | 18 | ## Usage 19 | 20 | ``` 21 | # openvpn NAME [OpenVPN args...] 22 | # e.g. 23 | openvpn foo https://vpn.example.com 24 | 25 | # openconnect NAME [OpenConnect args...] 26 | # e.g. 27 | openconnect bar https://vpn.example.com 28 | ``` 29 | 30 | The first argument is an arbitrary name that you give your VPN connection. This is used in the Docker container names and the SSH config file. The rest of the arguments are passed to the VPN client. Each example above will connect to a VPN located at vpn.example.com. 31 | 32 | Once connected, you will see a message telling you which ports are available and the name of the ssh config profile. 33 | 34 | ``` 35 | ============================================ 36 | SSH Port: 2222 37 | SOCKS Proxy Port: 1080 38 | HTTP Proxy Port: 1088 39 | Use: ssh foo 40 | ============================================ 41 | ``` 42 | 43 | I recommend using a proxy switcher browser extension like one of the following. This allows you to quickly switch proxies on/off or tunnel certain websites through a proxy while letting all other traffic go through your default gateway. 44 | * Proxy SwitchyOmega [[source]](https://github.com/FelisCatus/SwitchyOmega) [[Chrome]](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif) [[Firefox]](https://addons.mozilla.org/en-US/firefox/addon/switchyomega/) 45 | * FoxyProxy Standard [[source]](https://github.com/foxyproxy/firefox-extension) [[Firefox]](https://addons.mozilla.org/en-US/firefox/addon/foxyproxy-standard/) 46 | 47 | ### OpenVPN Config File 48 | 49 | ``` 50 | openvpn foo 51 | ``` 52 | 53 | To connect to the `foo` VPN put your config file at `~/.vpn/foo.ovpn` and then you can run `openvpn foo` to automatically use the corresponding config file. 54 | 55 | You can optionally put your credentials in `~/.vpn/foo.creds`. The username goes on the first line and the password on the second line. This gives up some security for the convenience of not having to enter your username and password. You will still be prompted for your 2FA code if your VPN endpoint requires it. You can run `chmod 600 ~/.vpn/foo.creds` to ensure only the file owner can read it. 56 | 57 | ### OpenConnect Profile 58 | 59 | OpenConnect offers an additional interactive command `openconnect_new_profile` which will guide you through a creation of a configuration profile. Once created, the profile is saved in `~/.vpn/NAME.profile` and `~/.vpn/NAME.secret`. To connect using a profile you can simply use `openconnect NAME` and the VPN connection will be established without any interaction. Currently, the following options are supported: 60 | 61 | - Hostname & optional port 62 | - Username authentication 63 | - with password 64 | - without password 65 | - with password & external 2-factor authentication 66 | - Connection group 67 | 68 | If you need custom configs for the openconnect client, you can create a file called `~/.vpn/foo.config` where you can 69 | use the wide range of configuration available at the [openconnect documentation](https://www.infradead.org/openconnect/manual.html). 70 | The file would be mounted inside the container and passed to the CLI with `--config` option. 71 | 72 | ## Customizing 73 | 74 | You can customize options by setting the following environment variables. The defaults are shown below. 75 | 76 | * `BIND_INTERFACE`: 127.0.0.1 77 | * `SSH_PORT`: 2222 78 | * `SOCKS_PORT`: 1080 79 | * `HTTP_PROXY_PORT`: 1088 80 | * `AUTHORIZED_KEYS`: Any keys allowed to SSH as the current user to the current machine, any keys configured in `ssh-agent`, and any keys found in `~/.ssh/*.pub`. 81 | 82 | ### Custom hosts 83 | 84 | In order to have custom hostname resolution done inside the container, you can add a `~/.vpn/NAME.hosts`, `NAME` being 85 | the profile config for either openconnect or openvpn. The format of the files follows the same standard as your 86 | /etc/hosts file: 87 | 88 | ``` 89 | my-custom-hostname 1.1.1.1 90 | ``` 91 | 92 | The hosts will then be added one by one to the docker command args, which would then edit the `/etc/hosts` file inside 93 | the container. See docker [--add-host option](https://docs.docker.com/reference/cli/docker/container/run/#add-host) for 94 | more information. 95 | 96 | ### Custom ENV 97 | 98 | You can add a custom env that is then passed to the docker cli using the file `~/.vpn/NAME.env`, `NAME` being 99 | the profile config for either openconnect or openvpn. See 100 | [--env-file option](https://docs.docker.com/compose/environment-variables/set-environment-variables/#substitute-with---env-file) 101 | for more information. 102 | 103 | ### Custom mounts 104 | 105 | To mount custom files or folders on the container, add a file `~/.vpn/NAME.mounts`, `NAME` being the profile for either 106 | openconnect or openvpn. The file follows the same format as the hosts file, where the first element is the local file, 107 | and the second is the remote file: 108 | 109 | ``` 110 | /local/file/to/be/mounted /container/mount/point 111 | ``` 112 | 113 | Please note that **neither of the file paths can contain spaces.** 114 | 115 | ### Advanced Forwarding 116 | 117 | docker-vpn provides all the power of an OpenSSH server. For example: 118 | 119 | * Dynamic port forwarding (SOCKS proxy) `ssh -D 1080 foo` - Starts a socks5 proxy on port 1080. Connections using this proxy will be tunneled through SSH into the container and then tunneled to the `foo` network through the VPN client. 120 | * Local port forwarding `ssh -L 8080:private.foo.com:80 foo` - Forwards port 80 on private.foo.com so that you can access it from localhost:8080. 121 | * Jump hosts `ssh -J foo user@private.foo.com` - Allows connecting via SSH to a remote server private.foo.com that is not directly accessible but is accessible by using the docker-vpn `foo` as a jump host. (Requires OpenSSH 7.3) 122 | * TUN/TAP support - SSH has [builtin tunneling support](https://wiki.archlinux.org/index.php/VPN_over_SSH#OpenSSH's_built_in_tunneling). This is similar to just connecting directly with OpenVPN or OpenConnect software, but gives you the power (and responsibility) to configure your own routing. 123 | 124 | ## Limitations 125 | - If you have multiple VPNs you want to connect to at once, you have to choose ports that do not conflict. 126 | - VPN configurations can be wildly different. I created these to make my specific use case easier. Other configurations may require passing in your own command line options and adding your own volume mounts. 127 | 128 | ## Credits 129 | - https://github.com/Praqma/alpine-sshd 130 | - https://github.com/vimagick/dockerfiles/blob/master/openconnect/Dockerfile 131 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -s /root/docker-vpn_keys ]; then 4 | echo "Copying keys to /root/.ssh/authorized_keys" 5 | cp -v /root/docker-vpn_keys /root/.ssh/authorized_keys 6 | fi 7 | 8 | if [ ! -s /root/.ssh/authorized_keys ]; then 9 | if [ -z "${AUTHORIZED_KEYS}" ]; then 10 | echo "Need your ssh public key as AUTHORIZED_KEYS env variable. Abnormal exit ..." 11 | exit 1 12 | fi 13 | 14 | echo "Populating /root/.ssh/authorized_keys with the value from AUTHORIZED_KEYS env variable ..." 15 | echo "${AUTHORIZED_KEYS}" > /root/.ssh/authorized_keys 16 | fi 17 | 18 | chown root /root/.ssh/authorized_keys 19 | chmod 600 /root/.ssh/authorized_keys 20 | 21 | # echo "Generating new host keys as necessary..." 22 | # ssh-keygen -A 23 | echo "Generating new host keys as necessary..." 24 | ssh-keygen -A 25 | 26 | # move internal key so that ssh localhost works inside the container 27 | cp /etc/ssh/ssh_host_ed25519_key /root/.ssh/id_ed25519 28 | cat /etc/ssh/ssh_host_ed25519_key.pub >> /root/.ssh/authorized_keys 29 | 30 | echo "Starting supervisor..." 31 | /usr/bin/supervisord --configuration=/etc/supervisord.conf --logfile=/dev/null 32 | 33 | # Execute the command passed to docker (likely a VPN connection command) 34 | exec "$@" 35 | -------------------------------------------------------------------------------- /etc/ssh/sshd_config: -------------------------------------------------------------------------------- 1 | # $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $ 2 | 3 | # This is the sshd server system-wide configuration file. See 4 | # sshd_config(5) for more information. 5 | 6 | # This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin 7 | 8 | # The strategy used for options in the default sshd_config shipped with 9 | # OpenSSH is to specify options with their default value where 10 | # possible, but leave them commented. Uncommented options override the 11 | # default value. 12 | 13 | #Port 22 14 | #AddressFamily any 15 | #ListenAddress 0.0.0.0 16 | #ListenAddress :: 17 | 18 | #HostKey /etc/ssh/ssh_host_rsa_key 19 | #HostKey /etc/ssh/ssh_host_ecdsa_key 20 | #HostKey /etc/ssh/ssh_host_ed25519_key 21 | 22 | # Ciphers and keying 23 | #RekeyLimit default none 24 | 25 | # Logging 26 | #SyslogFacility AUTH 27 | #LogLevel INFO 28 | 29 | # Authentication: 30 | 31 | #LoginGraceTime 2m 32 | #PermitRootLogin prohibit-password 33 | #StrictModes yes 34 | #MaxAuthTries 6 35 | #MaxSessions 10 36 | 37 | #PubkeyAuthentication yes 38 | 39 | # The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 40 | # but this is overridden so installations will only check .ssh/authorized_keys 41 | AuthorizedKeysFile .ssh/authorized_keys 42 | 43 | #AuthorizedPrincipalsFile none 44 | 45 | #AuthorizedKeysCommand none 46 | #AuthorizedKeysCommandUser nobody 47 | 48 | # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts 49 | #HostbasedAuthentication no 50 | # Change to yes if you don't trust ~/.ssh/known_hosts for 51 | # HostbasedAuthentication 52 | #IgnoreUserKnownHosts no 53 | # Don't read the user's ~/.rhosts and ~/.shosts files 54 | #IgnoreRhosts yes 55 | 56 | # To disable tunneled clear text passwords, change to no here! 57 | PasswordAuthentication no 58 | #PermitEmptyPasswords no 59 | 60 | # Change to no to disable s/key passwords 61 | #ChallengeResponseAuthentication yes 62 | 63 | # Kerberos options 64 | #KerberosAuthentication no 65 | #KerberosOrLocalPasswd yes 66 | #KerberosTicketCleanup yes 67 | #KerberosGetAFSToken no 68 | 69 | # GSSAPI options 70 | #GSSAPIAuthentication no 71 | #GSSAPICleanupCredentials yes 72 | 73 | # Set this to 'yes' to enable PAM authentication, account processing, 74 | # and session processing. If this is enabled, PAM authentication will 75 | # be allowed through the ChallengeResponseAuthentication and 76 | # PasswordAuthentication. Depending on your PAM configuration, 77 | # PAM authentication via ChallengeResponseAuthentication may bypass 78 | # the setting of "PermitRootLogin without-password". 79 | # If you just want the PAM account and session checks to run without 80 | # PAM authentication, then enable this but set PasswordAuthentication 81 | # and ChallengeResponseAuthentication to 'no'. 82 | #UsePAM no 83 | 84 | #AllowAgentForwarding yes 85 | # Feel free to re-enable these if your use case requires them. 86 | AllowTcpForwarding yes 87 | GatewayPorts clientspecified 88 | X11Forwarding no 89 | #X11DisplayOffset 10 90 | #X11UseLocalhost yes 91 | #PermitTTY yes 92 | #PrintMotd yes 93 | #PrintLastLog yes 94 | #TCPKeepAlive yes 95 | #PermitUserEnvironment no 96 | #Compression delayed 97 | #ClientAliveInterval 0 98 | #ClientAliveCountMax 3 99 | #UseDNS no 100 | #PidFile /run/sshd.pid 101 | #MaxStartups 10:30:100 102 | PermitTunnel yes 103 | #ChrootDirectory none 104 | #VersionAddendum none 105 | 106 | # no default banner path 107 | #Banner none 108 | 109 | # override default of no subsystems 110 | Subsystem sftp /usr/lib/ssh/sftp-server 111 | 112 | # Example of overriding settings on a per-user basis 113 | #Match User anoncvs 114 | # X11Forwarding no 115 | # AllowTcpForwarding no 116 | # PermitTTY no 117 | # ForceCommand cvs server -------------------------------------------------------------------------------- /etc/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/run/supervisor.sock 3 | 4 | [supervisord] 5 | pidfile=/run/supervisord.pid 6 | 7 | ; The rpcinterface:supervisor section must remain in the config file for 8 | ; RPC (supervisorctl/web interface) to work. Additional interfaces may be 9 | ; added by defining them in separate [rpcinterface:x] sections. 10 | 11 | [rpcinterface:supervisor] 12 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 13 | 14 | ; The supervisorctl section configures how supervisorctl will connect to 15 | ; supervisord. configure it match the settings in either the unix_http_server 16 | ; or inet_http_server section. 17 | 18 | [supervisorctl] 19 | serverurl=unix:///run/supervisor.sock 20 | 21 | # see https://catonmat.net/linux-socks5-proxy for explanation, it's a lot faster than pproxy 22 | [program:sshproxy] 23 | command=ssh -o "StrictHostKeyChecking=no" -N -D 0.0.0.0:1080 localhost 24 | # you can also fallback to pproxy for socks5 proxy, if the ssh approach does not work 25 | # [program:pproxy] 26 | # command=/usr/bin/pproxy -l socks5://:1080 27 | 28 | [program:httpproxy] 29 | command=/usr/bin/pproxy -l http://:1088 30 | 31 | [program:sshd] 32 | command=/usr/sbin/sshd -De -------------------------------------------------------------------------------- /vpn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | openvpn() { 4 | local vpnName="$1"; shift 5 | if [ -z "$vpnName" ]; then 6 | echo "VPN name must be provided" 7 | return 8 | fi 9 | # listen on localhost by default 10 | local bindIf="${BIND_INTERFACE:-127.0.0.1}" 11 | local socksPort="${SOCKS_PORT:-1080}" 12 | local httpProxyPort="${HTTP_PROXY_PORT:-1088}" 13 | local sshPort="${SSH_PORT:-2222}" 14 | local authorizedKeys="${AUTHORIZED_KEYS}" 15 | 16 | local vpnConfig="$HOME/.vpn" 17 | local dockerImage="ethack/vpn" 18 | 19 | # AUTHORIZED_KEYS not specified. Use some defaults. 20 | if [ -z "$authorizedKeys" ]; then 21 | # add any key allowed to ssh in as the current user 22 | if [ -f "$HOME/.ssh/authorized_keys" ]; then 23 | printf -v authorizedKeys "$(cat "$HOME/.ssh/authorized_keys")\n" 24 | fi 25 | # add all keys currently registered with ssh-agent 26 | if command -v ssh-add >/dev/null; then 27 | printf -v authorizedKeys "$(ssh-add -L)\n" 28 | fi 29 | # append any public key files found in the user's .ssh directory 30 | authorizedKeys+=$(find "$HOME/.ssh/" -type f -name '*.pub' -exec cat {} \;) 31 | fi 32 | 33 | local dockerCmd=("docker" "run") 34 | local vpnCmd=("openvpn") 35 | dockerCmd+=("--rm" "--name" "vpn-$vpnName") 36 | dockerCmd+=("--hostname" "vpn-$vpnName") 37 | dockerCmd+=("--interactive" "--tty") 38 | dockerCmd+=("--cap-add" "NET_ADMIN") 39 | dockerCmd+=("--device" "/dev/net/tun") 40 | dockerCmd+=("--publish" "$bindIf:$sshPort:22") 41 | dockerCmd+=("--publish" "$bindIf:$socksPort:1080") 42 | dockerCmd+=("--publish" "$bindIf:$httpProxyPort:3128") 43 | dockerCmd+=("--env" "AUTHORIZED_KEYS=$authorizedKeys") 44 | if [ -f "$vpnConfig/$vpnName.ovpn" ]; then 45 | dockerCmd+=("--mount" "type=bind,src=$vpnConfig/$vpnName.ovpn,dst=/vpn/config,readonly=true") 46 | vpnCmd+=("--config" "/vpn/config") 47 | fi 48 | if [ -f "$vpnConfig/$vpnName.creds" ]; then 49 | dockerCmd+=("--mount" "type=bind,src=$vpnConfig/$vpnName.creds,dst=/vpn/creds,readonly=true") 50 | vpnCmd+=("--auth-user-pass" "/vpn/creds") 51 | vpnCmd+=("--auth-retry" "interact") 52 | fi 53 | if [ -f "$vpnConfig/$vpnName.env" ]; then 54 | dockerCmd+=("--env-file" "$vpnConfig/$vpnName.env") 55 | fi 56 | if [ -f "$vpnConfig/$vpnName.hosts" ]; then 57 | while IFS= read -r line || [ -n "$line" ]; do 58 | # Skip commented lines and empty lines 59 | case "$line" in 60 | \#* | "") 61 | continue 62 | ;; 63 | esac 64 | hostname=$(echo "$line" | awk '{print $2}') 65 | ip=$(echo "$line" | awk '{print $1}') 66 | dockerCmd+=("--add-host" "$ip:$hostname") 67 | done < "$vpnConfig/$vpnName.hosts" 68 | fi 69 | # add custom mounts 70 | if [ -f "$vpnConfig/$vpnName.mounts" ]; then 71 | while IFS= read -r line || [ -n "$line" ]; do 72 | # Skip commented lines and empty lines 73 | case "$line" in 74 | \#* | "") 75 | continue 76 | ;; 77 | esac 78 | file_remote=$(echo "$line" | awk '{print $2}') 79 | file_local=$(echo "$line" | awk '{print $1}') 80 | dockerCmd+=("--mount" "type=bind,src=$file_local,dst=$file_remote,readonly=true") 81 | done < "$vpnConfig/$vpnName.mounts" 82 | fi 83 | dockerCmd+=("$dockerImage") 84 | 85 | # append any extra args provided 86 | vpnCmd+=("$@") 87 | # display help if there are no arguments at this point 88 | if [ ${#vpnCmd[@]} -eq 1 ]; then 89 | vpnCmd+=("--help") 90 | fi 91 | 92 | setup-ssh-config.d 93 | ssh-config "$vpnName" "$sshPort" > "$HOME/.ssh/config.d/vpn-$vpnName" 94 | 95 | echo "============================================" 96 | echo "SSH Port: $sshPort (customize with SSH_PORT)" 97 | echo "SOCKS Proxy Port: $socksPort (customize with SOCKS_PORT)" 98 | echo "HTTP Proxy Port: $httpProxyPort (customize with HTTP_PROXY_PORT)" 99 | echo "Use: ssh $vpnName" 100 | echo "============================================" 101 | 102 | "${dockerCmd[@]}" "${vpnCmd[@]}" 103 | } 104 | 105 | openconnect() { 106 | local vpnName="$1"; shift 107 | if [ -z "$vpnName" ]; then 108 | echo "VPN name must be provided" 109 | return 110 | fi 111 | # listen on localhost by default 112 | local bindIf="${BIND_INTERFACE:-127.0.0.1}" 113 | local socksPort="${SOCKS_PORT:-1080}" 114 | local httpProxyPort="${HTTP_PROXY_PORT:-1088}" 115 | local sshPort="${SSH_PORT:-2222}" 116 | local authorizedKeys="${AUTHORIZED_KEYS}" 117 | 118 | local vpnConfig="$HOME/.vpn" 119 | local vpnProfile="$vpnConfig/$vpnName.profile" 120 | local vpnSecret="$vpnConfig/$vpnName.secret" 121 | local dockerImage="ethack/vpn" 122 | 123 | # AUTHORIZED_KEYS not specified. Use some defaults. 124 | if [ -z "$authorizedKeys" ]; then 125 | # add any key allowed to ssh in as the current user 126 | if [ -f "$HOME/.ssh/authorized_keys" ]; then 127 | printf -v authorizedKeys "$(cat "$HOME/.ssh/authorized_keys")\n" 128 | fi 129 | # add all keys currently registered with ssh-agent 130 | if command -v ssh-add >/dev/null; then 131 | printf -v authorizedKeys "$(ssh-add -L)\n" 132 | fi 133 | # append any public key files found in the user's .ssh directory 134 | authorizedKeys+=$(find "$HOME/.ssh/" -type f -name '*.pub' -exec cat {} \;) 135 | fi 136 | 137 | local dockerCmd=("docker" "run") 138 | local vpnCmd=("openconnect") 139 | dockerCmd+=("--rm" "--name" "vpn-$vpnName") 140 | dockerCmd+=("--hostname" "vpn-$vpnName") 141 | dockerCmd+=("--interactive") 142 | dockerCmd+=("--cap-add" "NET_ADMIN") 143 | dockerCmd+=("--device" "/dev/net/tun") 144 | dockerCmd+=("--publish" "$bindIf:$sshPort:22") 145 | dockerCmd+=("--publish" "$bindIf:$socksPort:1080") 146 | dockerCmd+=("--publish" "$bindIf:$httpProxyPort:1088") 147 | dockerCmd+=("--env" "AUTHORIZED_KEYS=$authorizedKeys") 148 | if [ -f "$vpnConfig/$vpnName.xml" ]; then 149 | dockerCmd+=("--mount" "type=bind,src=$vpnConfig/$vpnName.xml,dst=/vpn/config,readonly=true") 150 | vpnCmd+=("--xmlconfig" "/vpn/config") 151 | fi 152 | if [ -f "$vpnConfig/$vpnName.config" ]; then 153 | dockerCmd+=("--mount" "type=bind,src=$vpnConfig/$vpnName.config,dst=/vpn/openconnect.config,readonly=true") 154 | vpnCmd+=("--config" "/vpn/openconnect.config") 155 | fi 156 | if [ -f "$vpnConfig/$vpnName.env" ]; then 157 | dockerCmd+=("--env-file" "$vpnConfig/$vpnName.env") 158 | fi 159 | if [ -f "$vpnConfig/$vpnName.hosts" ]; then 160 | while IFS= read -r line || [ -n "$line" ]; do 161 | # Skip commented lines and empty lines 162 | case "$line" in 163 | \#* | "") 164 | continue 165 | ;; 166 | esac 167 | hostname=$(echo "$line" | awk '{print $2}') 168 | ip=$(echo "$line" | awk '{print $1}') 169 | dockerCmd+=("--add-host" "$ip:$hostname") 170 | done < "$vpnConfig/$vpnName.hosts" 171 | fi 172 | # add custom mounts 173 | if [ -f "$vpnConfig/$vpnName.mounts" ]; then 174 | while IFS= read -r line || [ -n "$line" ]; do 175 | # Skip commented lines and empty lines 176 | case "$line" in 177 | \#* | "") 178 | continue 179 | ;; 180 | esac 181 | file_remote=$(echo "$line" | awk '{print $2}') 182 | file_local=$(echo "$line" | awk '{print $1}') 183 | dockerCmd+=("--mount" "type=bind,src=$file_local,dst=$file_remote") 184 | done < "$vpnConfig/$vpnName.mounts" 185 | fi 186 | 187 | if [ -f "${vpnProfile}" ]; then 188 | source "${vpnProfile}" 189 | vpnCmd+=("${OC_HOST}") 190 | vpnCmd+=("--user" "${OC_USER}") 191 | 192 | if [ -f "${vpnSecret}" ]; then 193 | vpnCmd+=("--passwd-on-stdin") 194 | else 195 | vpnCmd+=("--no-passwd") 196 | fi 197 | if ! [ -z "{$OC_GROUP}" ]; then 198 | vpnCmd+=("--authgroup" "${OC_GROUP}") 199 | fi 200 | fi 201 | 202 | # append any extra args provided 203 | vpnCmd+=("$@") 204 | # display help if there are no arguments at this point 205 | if [ ${#vpnCmd[@]} -eq 1 ]; then 206 | vpnCmd+=("--help") 207 | fi 208 | 209 | setup-ssh-config.d 210 | ssh-config "$vpnName" "$sshPort" > "$HOME/.ssh/config.d/vpn-$vpnName" 211 | chmod 600 "$HOME/.ssh/config.d/vpn-$vpnName" 212 | 213 | echo "============================================" 214 | echo "SSH Port: $sshPort (customize with SSH_PORT)" 215 | echo "SOCKS Proxy Port: $socksPort (customize with SOCKS_PORT)" 216 | echo "HTTP Proxy Port: $httpProxyPort (customize with HTTP_PROXY_PORT)" 217 | echo "Use: ssh $vpnName" 218 | echo "============================================" 219 | 220 | if [ -f "${vpnSecret}" ]; then 221 | dockerCmd+=("--interactive") 222 | dockerCmd+=("$dockerImage") 223 | cat "${vpnSecret}" - | "${dockerCmd[@]}" "${vpnCmd[@]}" 224 | else 225 | dockerCmd+=("--interactive" "--tty") 226 | dockerCmd+=("$dockerImage") 227 | "${dockerCmd[@]}" "${vpnCmd[@]}" 228 | fi 229 | } 230 | 231 | openconnect_new_profile() { 232 | echo "This tool will create automatic OpenConnect profile to allow automatic connections" 233 | echo 234 | 235 | echo -n "Name for the profile: " 236 | read -r vpnProfile 237 | if ! [[ "${vpnProfile}" =~ ^[A-Za-z0-9_]+$ ]]; then 238 | echo "Profile name should only contain letters, numbers, and underscores!" 239 | return 1 240 | fi 241 | local vpnProfilePath="$HOME/.vpn/${vpnProfile}.profile" 242 | if [[ -f "${vpnProfilePath}" ]]; then 243 | echo "Profile \"${vpnProfile}\" already exists in ${vpnProfilePath}" 244 | return 1 245 | fi 246 | 247 | echo -n "Hostname: " 248 | read -r vpnHost 249 | 250 | echo -n "Port [443]: " 251 | read -r vpnPort 252 | 253 | echo -n "Username: " 254 | read -r vpnUser 255 | 256 | echo -n "Password: " 257 | read -s -r vpnPass 258 | echo 259 | 260 | local vpnHostPort="${vpnHost}" 261 | if [[ ! -z "${vpnPort}" ]]; then 262 | vpnHostPort+="${vpnPort}" 263 | fi 264 | echo 265 | echo "Some VPNs require group code. Go to https://${vpnHostPort}/ and see if there's a \"GROUP\" dropdown present. It will show all possible group codes. If there's no such dropdown leave this field empty." 266 | echo -n "Group: " 267 | read -r vpnGroup 268 | 269 | echo 270 | echo "If your VPN requires two-factor authentication you need to specify its type. Usually it will be one of the following: pin, push, phone, sms. If your VPN doesn't use 2FA leave this field empty." 271 | echo -n "2FA Type: " 272 | read -r vpn2FaType 273 | 274 | printf "OC_HOST=%q\n" "${vpnHostPort}" >> "${vpnProfilePath}" 275 | printf "OC_USER=%q\n" "${vpnUser}" >> "${vpnProfilePath}" 276 | printf "OC_GROUP=%q\n" "${vpnGroup}" >> "${vpnProfilePath}" 277 | 278 | local vpnSecretPath="$HOME/.vpn/${vpnProfile}.secret" 279 | echo "${vpnPass}" > "${vpnSecretPath}" 280 | if ! [ -z "${vpn2FaType}" ]; then 281 | echo "${vpn2FaType}" >> "${vpnSecretPath}" 282 | fi 283 | 284 | chmod 0400 "${vpnProfilePath}" 285 | chmod 0400 "${vpnSecretPath}" 286 | 287 | echo 288 | echo "Your new profile has been saved in ${vpnProfilePath} and ${vpnSecretPath}" 289 | echo "Connect by typing: openconnect ${vpnProfile}" 290 | } 291 | 292 | # Create and configure the .ssh/config.d directory if it's not already 293 | setup-ssh-config.d() { 294 | if ! grep -qFi -e 'Include config.d/*' -e 'Include ~/.ssh/config.d/*' "$HOME/.ssh/config"; then 295 | echo >> "$HOME/.ssh/config" 296 | # This allows the Include to be at the end of the file (i.e. not nested in a Host directive) 297 | echo 'Match all' >> "$HOME/.ssh/config" 298 | echo 'Include config.d/*' >> "$HOME/.ssh/config" 299 | fi 300 | mkdir -p "$HOME/.ssh/config.d/" 301 | } 302 | 303 | # Print the SSH config entry for the given name and port 304 | ssh-config() { 305 | local name="$1" 306 | local sshPort="$2" 307 | local user="root" 308 | local host="127.0.0.1" 309 | 310 | cat << EOF 311 | Host vpn-$name $name 312 | Hostname $host 313 | User $user 314 | Port $sshPort 315 | NoHostAuthenticationForLocalhost yes 316 | 317 | EOF 318 | } 319 | --------------------------------------------------------------------------------