about summary refs log tree commit diff
diff options
context:
space:
mode:
authormaride <maride@darknebu.la>2018-09-04 11:09:04 +0200
committermaride <maride@darknebu.la>2018-09-04 11:09:04 +0200
commite7be7b3a847beddccc324067de6b0bfa24b3ef12 (patch)
treebfc40755646b473a4b3081dd5c6c46ac221a8a78
parent82c922d557f6628043ab771cdf10e4da9546347d (diff)
parenta46cb83df474e5f9c9be35a0f4543f85bf9f03ee (diff)
Merge branch 'access'
-rw-r--r--README.md2
-rw-r--r--hosted/access.html77
-rw-r--r--hosted/challenges.html3
-rw-r--r--hosted/index.html3
-rw-r--r--src/access.go176
-rw-r--r--src/container.go44
-rw-r--r--src/docker.go25
-rw-r--r--src/http.go39
-rw-r--r--src/main.go5
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