Starting OpenVPN client programmatically on socks-port using Go
Programmatically starting and stopping openvpn client using Go
Starting OpenVPN with Golang
Hey, I’m going to talk about how I’m starting an openvpn connection to my university OpenVPN server fully programmatically in Go. I’m using this to enable me to download content I need for studying by writing a crawler for it; that requires an openvpn connection sometimes which I do not want to start manually or up 24h/day. From that comes the programmatic idea.
OpenVPN
OpenVPN is software which allows running forward-proxies. The forward proxy is the OpenVPN server. This server takes requests and pretends it is the source, giving access to the web, and more importantly, to university websites here which are otherwise only available on university-LAN. The OpenVPN client is used to start a proxy locally, which can redirect all traffic or only a portion of traffic it receives to the OpenVPN server.
My goal is to setup the OpenVPN client so it acts as a socks proxy; only my crawler sends requests to the port where the crawler started the OpenVPN client on. A socks proxy is a proxy which talks the socks-protocol, which is specifically designed to enable forward proxying. The client, the OpenVPN client here, sends on tcp requests those requests to the server, the OpenVPN server here. The server executes the requests and proxies answer back to the client. More on the socks-protocol here, but thats the gist of it.
Setup without the programmatic part
My university gave me access to this OpenVPN file:
client
remote UniversityOpenVPNServerIP
port 1194
dev tun
proto udp
auth-user-pass
nobind
comp-lzo no
tls-version-min 1.2
<ca>
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
</ca>
verify-x509-name "C=DE, ST=Baden-Wuerttemberg, L=Karlsruhe, O=Karlsruhe Institute of Technology, OU=Steinbuch Centre for Computing, CN=ovpn.scc.kit.edu" subject
cipher AES-256-CBC
auth SHA384
verb 3
It works well the Global OpenVPN, where the local OpenVPN redirects all traffic to the OpenVPN server. It can be run like so:
sudo openvpn --config {{.ConfigFile}} --auth-user-pass {{.AuthFile}}
The Config File is the path to the file above and the AuthFile is a file containg the vpn authentication data, in the following format.
ClearUsername
ClearPassword
Note that username and password are not base64, but the clear characters. And also note the empty line after the password, which may be needed. With this we tell the client to not read on STDIN for the username and the password, but rather use the authentication data stored inside the AuthFile.
OpenVPN Client offering socks port configuration
For running the OpenVPN client as a socks proxy, I had to rewrite the OpenVPN configuration. The result is the following.
client
remote UniversityOpenVPNServerIP
port 1194
dev tun
proto udp
auth-user-pass kit.config
dhcp-option DNS 8.8.8.8
#ipv4
pull-filter ignore "dhcp-option DNS"
#ipv6
pull-filter ignore "dhcp-option DNS6"
nobind
comp-lzo no
tls-version-min 1.2
<ca>
-----BEGIN CERTIFICATE-----
[...]
BSeOE6Fuwg==
-----END CERTIFICATE-----
</ca>
verify-x509-name "C=DE, ST=Baden-Wuerttemberg, L=Karlsruhe, O=Karlsruhe Institute of Technology, OU=Steinbuch Centre for Computing, CN=ovpn.scc.kit.edu" subject
cipher AES-256-CBC
auth SHA384
verb 3
pull-filter ignore "ifconfig-ipv6"
pull-filter ignore "route-ipv6"
The server pushed its own DNS servers. I had to disable them with pull-filter
for DNS
because otherwise they sometimes failed to work correctly. I configured my own with dhcp-option DNS
.
Then I configured to ignore ipv6 completely with pull-filter ignore "ifconfig-ipv6
and pull-filter ignore "route-ipv6"
. Without it the OpenVPN somehow preferred to create ipv6 connections, but those often didn’t work correctly so it failed to connect. Now the OpenVPN client only retrieves the ipv4 connection information.
Starting the OpenVPN client manually in socks-mode
I had issues with starting the client locally on MacOS, which is why I moved the startup to a docker container, which exposes 1080 to point socks-connection onto. For this I used. the existing docker image kizzx2/docker-openvpn-client-socks. This image works quite well. I built my own version locally.
It starts the following way:
docker run -t --rm --device=/dev/net/tun --cap-add=NET_ADMIN --name openvpn-client --volume <folder-with-AuthFile-and-OpenVPN-Config>:/etc/openvpn/:ro -p 1080:1080 --dns 8.8.8.8 fabstu/docker-openvpn-client-socks
The options do the following:
-t
allocates a pseudo-tty to the container, which openvpn client does not start without.--rm
automatically destroys the container once it stops running.--device
and--cap-add=NET_ADMIN
allow the openvpn client inside the container to start accepting socks connections, which the client then redirects to the server.--name openvpn-client
gives the container a name. Now I can stop the container withdocker stop openvpn-client
.--volume <folder>:/etc/openvpn:ro
mounts the local folder into the container on the/etc/openvpn
mountpoint, in read-only mode. From there the openvpn client inside the container reads its configuration. Note that the config and the AuthFile have to be named a certain way.-p 1080:1080
exposes the local container-port 1080 to the whole host on host-port 1080.--dns 8.8.8.8
directs all dns traffic to this google-owned dns server. Without it the container dns does not work, and you getunknown error host unreachable
, which was a pain in the ass to debug.fabstu/docker-openvpn-client-socks
is the name I tagged the build with when I built it:docker build -t fabstu/docker-openvpn-client-socks .
.
How the docker image works is not that important. Just know that it works and that it should start correctly.
To test it you can use the following curl command. It should return the IP of the OpenVPN server network in json-format.
curl --socks5 127.0.0.1:1080 -s https://wtfismyip.com/json
Starting the docker container programmatically
I have the start command, the one I mentioned above. It works well in starting the container. Just use Golang’s os/exec
library. The issue I had was with stopping. The context.Context passed into cmd.CommandContext did not stop the container correctly. The reason is that the docker
program starts a new process, which then communicates with the docker daemon. But on cancelling the passed-in context only the parent docker-program gets killed, but not its children.
I tried some workarounds like killing the whole process group like mentioned here, but that did not work either. the docker daemon does not reliably stop the container once the docker client process tree gets killed. So in the end I replaced the ctx passed into cmd.CommandContext
with waiting on Done myself, and then calling docker stop openvpn-client
.
go func() {
<-outsideCtx.Done()
klog.V(5).InfoS("Stopping docker container",
"container_name", containerName,
)
err := r.RunE(context.Background(),
dash.TemplateSplitExpand(
`docker stop {{.}}`,
containerName,
),
)
if err != nil {
klog.ErrorS(err, "failed to stop docker container with docker stop",
"container_name", containerName,
)
return
}
klog.V(5).InfoS("Stopped docker container",
"container_name", containerName,
)
// Cancel the context used by docker run so we kill it if the prior
// stop didn't work. It won't be sucessful if the container didn't
// stop but let's give it the best effort.
cancel()
}()
Also note to future self: Do not try to kill sudo-programs. cancel()
does not work at all and fails silently. Explicit cmd.Process.Kill()
and similar kill calls return operation not permitted
. Try to avoid sudo at all costs, which is possible with docker here.
Using the started docker socks proxy in Go
The following code snippet contains the relevant parts. Credit to Stackoverflow and this Go Playground for showing how it works with Go. Note: There are also other ways to set a proxy, like with HTTP_PROXY
shown in an answer in the first link.
dialSocksProxy, err := proxy.SOCKS5("tcp", proxyAddress, nil, proxy.Direct)
if err != nil {
return nil, err
}
dialContext := func(ctx context.Context, network, address string) (net.Conn, error) {
return dialSocksProxy.DialContext(ctx, network, address)
}
tr := &http.Transport{
DialContext: dialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
DisableKeepAlives: true,
}
klog.V(5).InfoS("Created myhttp client using socks5 proxy url",
"proxy_url", proxyAddress,
)
client := &http.Client{
Transport: tr,
}
// Use client to make http requests.
//
// NOTE: http.Get does not use this client, so that WON'T BE REDIRECTED THROUGH THE SOCKS-PROXY.