Skip to content

Commit bc28ca3

Browse files
committed
Basic working form (mostly) in golang
Needs a _lot_ of cleanup
1 parent f84e5db commit bc28ca3

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module aws-vpn-wrapper
2+
3+
go 1.15

main.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"flag"
7+
"fmt"
8+
"io/ioutil"
9+
"log"
10+
"net"
11+
"net/http"
12+
"net/url"
13+
"os"
14+
"os/exec"
15+
"runtime"
16+
"strings"
17+
)
18+
19+
/*
20+
* Wrapper for doing the open-vpn dance for aws vpns that use SAML auth
21+
* Step 1: Attempt an openvpn connection using static names; that will give us the saml url
22+
* Step 2: Visit the SAML url in a web browser and catch the response
23+
* Step 3: Re-run openvpn with the new auth
24+
*/
25+
26+
func main() {
27+
sourceConfigFile := flag.String("config", "~/.awsvpn.conf", "Source aws vpn config file")
28+
flag.Parse()
29+
configFilename, serverURL, serverPort, err := createTempConfigFile(*sourceConfigFile)
30+
ips, err := net.LookupIP("dns." + serverURL) // have to use "random" subdomain
31+
if err != nil || len(ips) == 0 {
32+
fmt.Fprintf(os.Stderr, "Could not get IPs for VPN server : %v\n", err)
33+
os.Exit(1)
34+
}
35+
36+
serverURL = ips[0].String()
37+
if err != nil {
38+
log.Fatal(err)
39+
}
40+
defer os.Remove(configFilename)
41+
fmt.Printf("Starting vpn to %s:%s\n", serverURL, serverPort)
42+
//Connect once to find the saml auth url to use
43+
samlAuthpage, sid, err := initalcontactFindSAMLURL(configFilename, serverURL, serverPort)
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
fmt.Println("Opening webpage to auth now", samlAuthpage)
48+
openbrowser(samlAuthpage)
49+
a := newSAMLAuth(sid, serverURL, serverPort, configFilename)
50+
a.runHTTPServer()
51+
}
52+
53+
type SAMLAuth struct {
54+
samlResponseChan chan string
55+
sidID string
56+
server string
57+
port string
58+
confpath string
59+
}
60+
61+
func newSAMLAuth(sid, server, port, confpath string) *SAMLAuth {
62+
s := &SAMLAuth{samlResponseChan: make(chan string, 2), sidID: sid, server: server, port: port, confpath: confpath}
63+
return s
64+
}
65+
func (s *SAMLAuth) runHTTPServer() {
66+
go s.worker()
67+
http.HandleFunc("/", s.handleSAMLServer)
68+
log.Printf("Starting HTTP server at 127.0.0.1:35001")
69+
http.ListenAndServe("127.0.0.1:35001", nil)
70+
}
71+
72+
func (s *SAMLAuth) worker() {
73+
//Listens for events from saml http server and spawns openvpn as appropriate
74+
for {
75+
select {
76+
case auth, ok := <-s.samlResponseChan:
77+
if !ok {
78+
return
79+
}
80+
//we have authentication, lets spawn the correct openvpn
81+
fmt.Println("Starting the actual openvpn ")
82+
runOpenVPNAuthenticated(auth, s.sidID, s.server, s.port, s.confpath)
83+
84+
}
85+
}
86+
}
87+
func (s *SAMLAuth) handleSAMLServer(w http.ResponseWriter, r *http.Request) {
88+
switch r.Method {
89+
case "POST":
90+
if err := r.ParseForm(); err != nil {
91+
fmt.Fprintf(w, "ParseForm() err: %v", err)
92+
return
93+
}
94+
SAMLResponse := r.FormValue("SAMLResponse")
95+
if len(SAMLResponse) == 0 {
96+
log.Printf("SAMLResponse field is empty or not exists")
97+
return
98+
}
99+
s.samlResponseChan <- url.QueryEscape(SAMLResponse)
100+
log.Printf("Got SAMLResponse field")
101+
return
102+
default:
103+
fmt.Fprintf(w, "Error: POST method expected, %s recieved", r.Method)
104+
}
105+
}
106+
107+
func runOpenVPNAuthenticated(samlAuth, sid, server, serverPort, confpath string) {
108+
fmt.Printf("Running openvpn with SID:%s server %s:%s\n", sid, server, serverPort)
109+
destFile, err := ioutil.TempFile("", "aws_vpn_wrapper_config_*.password")
110+
if err != nil {
111+
return
112+
}
113+
commandInput := fmt.Sprintf("%s\r\nCRV1::%s::%s\r\n", "N/A", sid, samlAuth)
114+
destFile.WriteString(commandInput)
115+
destFile.Close()
116+
117+
cmd := exec.Command("sudo", "./openvpn-patched", "--config", confpath, "--remote", server, serverPort, "--auth-user-pass", destFile.Name())
118+
cmd.Stderr = os.Stderr
119+
cmd.Stdout = os.Stdout
120+
err = cmd.Start()
121+
cmd.Wait()
122+
cmd.Process.Kill()
123+
if err != nil {
124+
log.Fatal(err)
125+
}
126+
os.Exit(0)
127+
}
128+
func openbrowser(url string) {
129+
var err error
130+
131+
switch runtime.GOOS {
132+
case "linux":
133+
err = exec.Command("xdg-open", url).Start()
134+
case "windows":
135+
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
136+
case "darwin":
137+
err = exec.Command("open", url).Start()
138+
default:
139+
err = fmt.Errorf("unsupported platform")
140+
}
141+
if err != nil {
142+
log.Fatal(err)
143+
}
144+
145+
}
146+
147+
func initalcontactFindSAMLURL(confpath, server, serverPort string) (SAMLString, sid string, err error) {
148+
destFile, err := ioutil.TempFile("", "aws_vpn_wrapper_config_*.password")
149+
if err != nil {
150+
return
151+
}
152+
commandInput := fmt.Sprintf("%s\r\n%s\r\n", "N/A", "ACS::35001")
153+
destFile.WriteString(commandInput)
154+
destFile.Close()
155+
156+
cmd := exec.Command("./openvpn-patched", "--config", confpath, "--remote", server, serverPort, "--auth-user-pass", destFile.Name())
157+
var outb bytes.Buffer
158+
cmd.Stdout = &outb
159+
cmd.Stderr = os.Stderr
160+
err = cmd.Start()
161+
cmd.Wait()
162+
// We wait until we get response
163+
scanner := bufio.NewScanner(&outb)
164+
for scanner.Scan() {
165+
line := scanner.Text()
166+
if strings.Contains(line, "AUTH_FAILED,CRV1") {
167+
//This line is our saml url
168+
parts := strings.Split(line, "https://")
169+
SAMLString = "https://" + parts[1]
170+
parts = strings.Split(line, ":")
171+
sid = parts[6]
172+
}
173+
}
174+
cmd.Process.Kill()
175+
return
176+
}
177+
178+
//createTempConfigFile Creates a temporary config to use for authentication that has the server name parsed out
179+
// and this is returned seperately
180+
func createTempConfigFile(sourceFilePath string) (outputFilename string, server string, port string, err error) {
181+
//Read the source file in and strip the server path and port out, and copy to a temp file
182+
destFile, err := ioutil.TempFile("", "aws_vpn_wrapper_config_*.conf")
183+
if err != nil {
184+
return
185+
}
186+
outputWriter := bufio.NewWriter(destFile)
187+
defer outputWriter.Flush()
188+
defer destFile.Close()
189+
outputFilename = destFile.Name()
190+
//Read the source file in and copy all lines 1:1 except stripping server
191+
sourceFile, err := os.Open(sourceFilePath)
192+
if err != nil {
193+
return
194+
}
195+
defer sourceFile.Close()
196+
197+
scanner := bufio.NewScanner(sourceFile)
198+
for scanner.Scan() {
199+
line := scanner.Text()
200+
if strings.HasPrefix(line, "auth-retry") {
201+
//Strip
202+
} else if strings.HasPrefix(line, "resolv-retry") {
203+
//Strip
204+
} else if strings.HasPrefix(line, "auth-federate") {
205+
//Strip
206+
} else if strings.HasPrefix(line, "remote ") {
207+
// Split this apart to find the hostname and the port
208+
fields := strings.Fields(line)
209+
server = fields[1]
210+
port = fields[2]
211+
212+
} else {
213+
_, err = outputWriter.WriteString(line + "\n")
214+
if err != nil {
215+
return
216+
}
217+
}
218+
}
219+
outputWriter.Flush()
220+
if err = scanner.Err(); err != nil {
221+
return
222+
}
223+
return
224+
}

0 commit comments

Comments
 (0)