diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | hosted/access.html | 77 | ||||
-rw-r--r-- | hosted/challenges.html | 3 | ||||
-rw-r--r-- | hosted/index.html | 3 | ||||
-rw-r--r-- | src/access.go | 176 | ||||
-rw-r--r-- | src/container.go | 44 | ||||
-rw-r--r-- | src/docker.go | 25 | ||||
-rw-r--r-- | src/http.go | 39 | ||||
-rw-r--r-- | src/main.go | 5 |
9 files changed, 352 insertions, 22 deletions
diff --git a/README.md b/README.md index 89b2017..bef50bc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ This executable needs some parameters to work properly: | `-accessCode` | Yes | Access code for the user. *Default: AllYourCodesAreBelongToUs* | | `-sessionSalt` | Yes | Variable to salt the session token generator with. | | `-seedFile` | Yes | JSON file to read challenge information from. | +| `-vpnRemoteAddress` | Yes | Address the VPN will run on, as rendered into the client VPN configuration file. | +| `-vpnRemotePort` | No | Port the VPN will run on | ## Seed file diff --git a/hosted/access.html b/hosted/access.html new file mode 100644 index 0000000..5a25fd0 --- /dev/null +++ b/hosted/access.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> + <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script> + <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> + <style type="text/css"> + body { + overflow-y: scroll; + } + </style> + </head> + <body> + <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4"> + <a class="navbar-brand" href="/">Companion</a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarCollapse"> + <ul class="navbar-nav mr-auto"> + <li class="nav-item"> + <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a> + </li> + <li class="nav-item active"> + <a class="nav-link" href="/access">Access</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/challenges">Challenges</a> + </li> + </ul> + <form class="form-inline mt-2 mt-md-0" action="/logout" method="post"> + <button class="btn btn-outline-danger my-2 my-sm-0" type="submit">Logout</button> + </form> + </div> + </nav> + <main class="container" role="main"> + <div class="card"> + <div class="card-header"> + Access + </div> + <div class="card-body"> + Access to the challenge containers is provided via OpenVPN. + <hr> + <button class="btn btn-secondary" onclick="loadConfig()">Reload</button> + <button class="btn btn-primary" onclick="downloadConfig()">Download</button> + <hr> + <pre><code id="config">Loading...</code></pre> + </div> + </div> + <br> + </main> + <script> + function loadConfig() { + $("#config").html("Loading..."); + $.get("/api/getAccess").done(function(data) { + var result = jQuery.parseJSON(data); + $("#config").text(result["credentials"]); + }); + } + + function downloadConfig() { + $.get("/api/getAccess").done(function(data) { + var result = jQuery.parseJSON(data); + var configBlob = new Blob([result["credentials"]], {'type':'application/x-openvpn-config'}); + window.location = URL.createObjectURL(configBlob); + }); + } + + $(document).ready( + function(){ + loadConfig() + } + ); + </script> + </body> +</html> \ No newline at end of file diff --git a/hosted/challenges.html b/hosted/challenges.html index e42d7f0..165b9a9 100644 --- a/hosted/challenges.html +++ b/hosted/challenges.html @@ -79,6 +79,9 @@ <li class="nav-item"> <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a> </li> + <li class="nav-item"> + <a class="nav-link" href="/access">Access</a> + </li> <li class="nav-item active"> <a class="nav-link" href="/challenges">Challenges</a> </li> diff --git a/hosted/index.html b/hosted/index.html index 0f40399..70244cc 100644 --- a/hosted/index.html +++ b/hosted/index.html @@ -16,6 +16,9 @@ <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> + <a class="nav-link" href="/access">Access</a> + </li> + <li class="nav-item"> <a class="nav-link" href="/challenges">Challenges</a> </li> </ul> diff --git a/src/access.go b/src/access.go new file mode 100644 index 0000000..6f9cd73 --- /dev/null +++ b/src/access.go @@ -0,0 +1,176 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "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" + "net/http" + "time" +) + +const( + vpnHostNetworkName = "vpnhostnet" +) + +var vpnContainerID string +var vpnNetworkID string +var vpnHostNetworkID 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) { + // Set up our context and Docker CLI connection + setupContext() + setupDockerCLI() + // Set up network + err = setupNetwork() + + if(err != nil) { + return err + } + + err = setupVPNHostNetwork() + + if err != nil { + return err + } + + // Create container + resp, err := dockerCli.ContainerCreate(dockerCtx, &container.Config{ + Image: "circus-vpn", + Env: []string{ + fmt.Sprintf("remoteAddress=%s", *remoteAddress), + fmt.Sprintf("remotePort=%d", *remotePort), + }, + ExposedPorts: map[nat.Port]struct{}{ + "1194/udp": {}, + }, + }, &container.HostConfig{ + Privileged: true, + PortBindings: nat.PortMap{ + "1194/udp": []nat.PortBinding{ + { + HostIP: "0.0.0.0", + HostPort: "1194", + }, + }, + }, + }, &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, vpnNetworkID, resp.ID, &network.EndpointSettings{}) + if err != nil { + return err + } + + // Start container + err = dockerCli.ContainerStart(dockerCtx, resp.ID, types.ContainerStartOptions{}) + if err != nil { + return err + } + + vpnContainerID = resp.ID + + return nil +} + +func stopVPN() { + setupContext() + setupDockerCLI() + + timeout := time.Second * 5 + dockerCli.ContainerStop(dockerCtx, vpnContainerID, &timeout) + + vpnContainerID = "" +} + +func setupNetwork() (error) { + setupContext() + setupDockerCLI() + + if vpnNetworkID == "" { + response, err := dockerCli.NetworkCreate(dockerCtx, VPNNetworkName, types.NetworkCreate{ + Internal: true, + }) + + if err != nil { + return err + } + + vpnNetworkID = response.ID + } + + return nil +} + +func setupVPNHostNetwork() (error) { + setupContext() + setupDockerCLI() + + if vpnHostNetworkID == "" { + response, err := dockerCli.NetworkCreate(dockerCtx, vpnHostNetworkName, types.NetworkCreate{ + Internal: false, + }) + + if err != nil { + return err + } + + vpnHostNetworkID = response.ID + } + + return nil +} + +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 + + for i := 0; i < 10; i++ { + certResponse, err = http.Get(fmt.Sprintf("http://%s:9999/", inspectJSON.NetworkSettings.Networks[VPNNetworkName].IPAddress)) + + if err == nil { + break + } + time.Sleep(time.Second) + } + + if err != nil { + return "", err + } + + buffer := make([]byte, 1024) + certResponse.Body.Read(buffer) + + return string(bytes.Trim(buffer, "\x00")), nil +} diff --git a/src/container.go b/src/container.go index 73912bf..c9a918f 100644 --- a/src/container.go +++ b/src/container.go @@ -1,39 +1,33 @@ package main import ( - "context" - "github.com/docker/docker/client" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types" "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" "time" ) +const ( + VPNNetworkName = "circus-vpnnet" +) + type ChallengeContainer struct { Challenge *Challenge ContainerID string IP string } -var ( - dockerCtx context.Context - dockerCli *client.Client -) - // Starts the container and returns its address and containerID if successful func (cc ChallengeContainer) startContainer() (address string, containerID string, err error) { - // Set up our context if there is none already set up - if dockerCtx == nil { - dockerCtx = context.Background() - } + // Set up our context and Docker CLI connection + setupContext() + setupDockerCLI() + // Set up network + err = setupNetwork() - // Set up our Docker CLI connection if there is not already one - if dockerCli == nil { - dockerCli, err = client.NewEnvClient() - - if err != nil { - return "", "", err - } + if err != nil { + return "", "", err } // Create container @@ -41,7 +35,13 @@ func (cc ChallengeContainer) startContainer() (address string, containerID strin Image: cc.Challenge.Container, Env: []string{fmt.Sprintf("FLAG=%s", cc.Challenge.Flag)}, Tty: false, - }, nil, nil, "") + }, nil, &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + VPNNetworkName: { + NetworkID: vpnNetworkID, + }, + }, + }, "") if err != nil { return "", "", err @@ -60,7 +60,7 @@ func (cc ChallengeContainer) startContainer() (address string, containerID strin } // Return IP, Container ID and error - return inspectJSON.NetworkSettings.IPAddress, resp.ID,nil + return inspectJSON.NetworkSettings.Networks[VPNNetworkName].IPAddress, resp.ID,nil } // Stops the container with a timeout of one second diff --git a/src/docker.go b/src/docker.go new file mode 100644 index 0000000..9bc667b --- /dev/null +++ b/src/docker.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/docker/docker/client" + "context" +) + +var ( + dockerCtx context.Context + dockerCli *client.Client +) + +func setupContext() { + if dockerCtx == nil { + dockerCtx = context.Background() + } +} + +func setupDockerCLI() (err error) { + if dockerCli == nil { + dockerCli, err = client.NewEnvClient() + } + + return err +} \ No newline at end of file diff --git a/src/http.go b/src/http.go index 287004f..c7ad213 100644 --- a/src/http.go +++ b/src/http.go @@ -30,10 +30,12 @@ func runHTTPServer() (error) { r.HandleFunc("/login", loginPostHandler).Methods("POST") r.HandleFunc("/logout", logoutHandler).Methods("POST") r.HandleFunc("/challenges", challengesHandler).Methods("GET") + r.HandleFunc("/access", accessHandler).Methods("GET") r.HandleFunc("/api/getChallenges", getChallengesHandler).Methods("GET") r.HandleFunc("/api/submitFlag", submitFlagHandler).Methods("POST") r.HandleFunc("/api/startContainer", startContainerHandler).Methods("POST") r.HandleFunc("/api/stopContainer", stopContainerHandler).Methods("POST") + r.HandleFunc("/api/getAccess", getAccessHandler).Methods("GET") address := fmt.Sprintf(":%d", *port) return http.ListenAndServe(address, r) @@ -163,6 +165,19 @@ func challengesHandler(w http.ResponseWriter, r *http.Request) { } } +// Host the access file +func accessHandler(w http.ResponseWriter, r *http.Request) { + session, cookieNotFoundError := r.Cookie("session") + + if cookieNotFoundError != nil || !isValidSession(session.Value) { + // either no session cookie found, or it contains an invalid session token. Redirect. + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + } else { + // valid session token found, redirect to frontpage + readFileToResponse(w, "/access.html") + } +} + func getChallengesHandler(w http.ResponseWriter, r *http.Request) { session, cookieNotFoundError := r.Cookie("session") @@ -282,3 +297,27 @@ func stopContainerHandler(w http.ResponseWriter, r *http.Request) { stopChallengeContainer(challengeName) } } + +// Returns the configuration for the VPN +func getAccessHandler(w http.ResponseWriter, r *http.Request) { + session, cookieNotFoundError := r.Cookie("session") + + if cookieNotFoundError != nil || !isValidSession(session.Value) { + // either no session cookie found, or it contains an invalid session token. Redirect. + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + } else { + // valid session token found, get credentials + credentials, err := getCertificate() + errorString := "" + + if err != nil { + errorString = err.Error() + } + + jsonAnswer, _ := json.Marshal(map[string]string{ + "error": errorString, + "credentials": credentials, + }) + w.Write([]byte(jsonAnswer)) + } +} diff --git a/src/main.go b/src/main.go index 9885957..ae50632 100644 --- a/src/main.go +++ b/src/main.go @@ -11,11 +11,16 @@ func main() { registerSessionFlags() registerCredentialsFlags() registerSeedFlags() + registerAccessFlags() flag.Parse() // Read challenges from file getChallengesFromSeedFile() + // Start our VPN container and network + startVPN() + defer stopVPN() + // Run HTTP server log.Fatalln(runHTTPServer()) } \ No newline at end of file |