package main import ( "bytes" "errors" "flag" "fmt" "net" "net/http" "os" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" ) const ( vpnContainerName = "registry.darknebu.la/circus/vpn" ) var vpnContainerID string var remoteAddress *string var remotePort *int func registerAccessFlags() { remoteAddress = flag.String("vpnRemoteAddress", "", "The remote domain name or IP the VPN will run on") remotePort = flag.Int("vpnRemotePort", 1194, "The port the VPN should listen on") } func startVPN() (err error) { // First: try to stop an existing container with that name, to avoid collision // This container may exist if we didn't properly clean up on the last run, e.g. if the companion crashed. stopVPN() // stopVPN() as first command in startVPN()... This looks strange, but is perfectly fine ;) // Set up our context and Docker CLI connection setupContext() setupDockerCLI() // Set up VPN host network if vpnHostNetworkID == "" { id, err := setupNetwork(getVPNNetworkName(), false) if err != nil { return err } vpnHostNetworkID = id } // Set up container network if containerNetworkID == "" { id, err := setupNetwork(getChallengeNetworkName(), true) if err != nil { return err } containerNetworkID = id } // Get subnet of challenge container network, to hand it over to our VPN container for routes inspectResp, err := dockerCli.NetworkInspect(dockerCtx, containerNetworkID, types.NetworkInspectOptions{}) if err != nil { return err } // Parse subnet (in CIDR notation) _, ipnet, err := net.ParseCIDR(inspectResp.IPAM.Config[0].Subnet) if err != nil { return err } // Create VPN container resp, err := dockerCli.ContainerCreate(dockerCtx, &container.Config{ Image: vpnContainerName, Env: []string{ fmt.Sprintf("remoteAddress=%s", *remoteAddress), fmt.Sprintf("remotePort=%d", *remotePort), fmt.Sprintf("subnet=%s", ipnet.IP.String()), fmt.Sprintf("subnetMask=%d.%d.%d.%d", ipnet.Mask[0], ipnet.Mask[1], ipnet.Mask[2], ipnet.Mask[3]), }, ExposedPorts: map[nat.Port]struct{}{ "1194/udp": {}, }, }, &container.HostConfig{ Privileged: true, PortBindings: nat.PortMap{ "1194/udp": []nat.PortBinding{ { HostIP: "0.0.0.0", HostPort: fmt.Sprintf("%d", *remotePort), }, }, }, }, &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ "startpoint": { NetworkID: vpnHostNetworkID, }, }, }, "") if err != nil { return err } // Attach container network to VPN container err = dockerCli.NetworkConnect(dockerCtx, containerNetworkID, resp.ID, &network.EndpointSettings{}) if err != nil { return err } // Start container err = dockerCli.ContainerStart(dockerCtx, resp.ID, types.ContainerStartOptions{}) if err != nil { return err } // We now need to do a little stunt. If the companion is started inside a container, it's not possible to dial to port 9999 of the VPN container. // However, getCertificate() requires that port 9999 of the VPN container hosts the configuration files for our client. // That means we need to attach our own container - thanks to --privileged mode - into the VPN container network. // We get the ID of our container from the "hostname" environment variable. That's a bit dirty, but works for the moment. TODO: solve this better. err = dockerCli.NetworkConnect(dockerCtx, vpnHostNetworkID, os.Getenv("HOSTNAME"), &network.EndpointSettings{}) if err != nil { return err } vpnContainerID = resp.ID return nil } func stopVPN() { setupContext() setupDockerCLI() timeout := time.Second * 5 dockerCli.ContainerStop(dockerCtx, vpnContainerID, &timeout) // We need to undo our stunt! Else, the network won't be deletable because "there are active endpoints" dockerCli.NetworkDisconnect(dockerCtx, vpnHostNetworkID, os.Getenv("HOSTNAME"), true) vpnContainerID = "" } func getCertificate() (string, error) { if vpnContainerID == "" { return "", errors.New("VPN container not up") } // Get IP of VPN container inspectJSON, err := dockerCli.ContainerInspect(dockerCtx, vpnContainerID) if err != nil { return "", err } // get certificate var certResponse *http.Response // retry for 10 seconds to dial to the VPN container for i := 0; i < 10; i++ { // Check if the VPN container is already part of our challenge container network if inspectJSON.NetworkSettings.Networks[getVPNNetworkName()] != nil { // it is - get the IP address and dial to it certResponse, err = http.Get(fmt.Sprintf("http://%s:9999/", inspectJSON.NetworkSettings.Networks[getVPNNetworkName()].IPAddress)) if err == nil { break } } time.Sleep(time.Second) } if err != nil || certResponse == nil { return "", err } buffer := make([]byte, 1024) certResponse.Body.Read(buffer) return string(bytes.Trim(buffer, "\x00")), nil }