Initial release
This commit is contained in:
commit
011115f9d6
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
wireguard-mfa.exe
|
2
Makefile
Normal file
2
Makefile
Normal file
@ -0,0 +1,2 @@
|
||||
wireguard-mfa.exe: main.go native.go
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags -H=windowsgui
|
8
README.md
Normal file
8
README.md
Normal file
@ -0,0 +1,8 @@
|
||||
Installation
|
||||
============
|
||||
1. Copy `wireguard-mfa.exe` to `C:\Program Files\Gibon\`
|
||||
2. Create the `C:\Program Files\Gibon\wg-spool\` directory and make sure users activating Wireguard tunnels have read- and write permissions to it.
|
||||
|
||||
Log file
|
||||
========
|
||||
If anything goes wrong, look in `C:\Program Files\Gibon\wg-spool\log.txt`.
|
10
go.mod
Normal file
10
go.mod
Normal file
@ -0,0 +1,10 @@
|
||||
module wireguard-mfa
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
golang.org/x/sys v0.7.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
)
|
||||
|
||||
require golang.org/x/crypto v0.8.0 // indirect
|
7
go.sum
Normal file
7
go.sum
Normal file
@ -0,0 +1,7 @@
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
138
main.go
Normal file
138
main.go
Normal file
@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// External
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
// Standard
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
const VERSION = "v1"
|
||||
const SPOOLDIR = "C:\\Program Files\\Gibon\\wg-spool"
|
||||
const DOMAIN = "https://vpn.gibonuddevalla.se"
|
||||
// const DOMAIN = "http://192.168.122.1:8000"
|
||||
|
||||
var (
|
||||
publicKeyPath string
|
||||
runInteractiveAuth bool
|
||||
connected bool
|
||||
printVersion bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&printVersion, "version", false, "print version and exit")
|
||||
flag.BoolVar(&runInteractiveAuth, "interactive", false, "run interactive authentication")
|
||||
flag.BoolVar(&connected, "connected", false, "postop, is connected")
|
||||
flag.Parse()
|
||||
|
||||
if printVersion {
|
||||
fmt.Println(VERSION)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func logError(where, e string) {
|
||||
fmt.Printf("ERROR: %s\n", e)
|
||||
fname := fmt.Sprintf("%s\\log.txt", SPOOLDIR)
|
||||
file, err := os.OpenFile(fname, os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
now := time.Now().String()
|
||||
file.Write([]byte(now))
|
||||
file.Write([]byte("\t"))
|
||||
file.Write([]byte(where))
|
||||
file.Write([]byte("\t"))
|
||||
file.Write([]byte(e))
|
||||
file.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
publicKeyPath = SPOOLDIR + "\\wireguard-mfa.url"
|
||||
iface := os.Getenv("WIREGUARD_TUNNEL_NAME")
|
||||
|
||||
// Run from PreUp, step 1.
|
||||
// Starts an interactive process, dropping privileges,
|
||||
// in order to have permission to open a browser in step 2.
|
||||
// Doesn't have permission to get public key.
|
||||
if !runInteractiveAuth && !connected {
|
||||
ex, _ := os.Executable()
|
||||
if err := StartProcessAsCurrentUser("", ex+" -interactive", ""); err != nil {
|
||||
logError("start_interactive", err.Error())
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Started from PreUp, step 2.
|
||||
// Doesn't have permission to get public key.
|
||||
if runInteractiveAuth {
|
||||
var err error
|
||||
start := time.Now()
|
||||
for {
|
||||
// Abort after 60 seconds to not have an eternal loop.
|
||||
if time.Since(start).Seconds() > 60 {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = os.ReadFile(publicKeyPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
continue
|
||||
}
|
||||
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
logError("read_publickey_file", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
cmd := exec.Command("rundll32.exe", "url.dll,OpenURL", publicKeyPath)
|
||||
cmd.Output()
|
||||
os.Remove(publicKeyPath)
|
||||
}
|
||||
|
||||
// Run from PostUp, step 3.
|
||||
// Has only permission to get public key, can't run browser.
|
||||
if connected {
|
||||
getPublicKey(iface)
|
||||
}
|
||||
}
|
||||
|
||||
func getPublicKey(iface string) {
|
||||
rxpPrivateKey := regexp.MustCompile("PrivateKey\\s*=\\s*(.*)")
|
||||
cmd := exec.Command("wg", "showconf", iface)
|
||||
out, _ := cmd.Output()
|
||||
privateKey := rxpPrivateKey.FindStringSubmatch(string(out))
|
||||
if len(privateKey) == 2 {
|
||||
pubkey, err := wgtypes.ParseKey(privateKey[1])
|
||||
if err != nil {
|
||||
logError("parse_privatekey", fmt.Sprintf("%s [%s]", err.Error(), privateKey[1]))
|
||||
return
|
||||
}
|
||||
|
||||
urlData := fmt.Sprintf(
|
||||
"[InternetShortcut]\r\nURL=%s/?key=%s\r\n",
|
||||
DOMAIN,
|
||||
url.QueryEscape(pubkey.PublicKey().String()),
|
||||
)
|
||||
os.WriteFile(publicKeyPath, []byte(urlData), 0644)
|
||||
logError("publickey", pubkey.PublicKey().String())
|
||||
} else {
|
||||
logError("get_publickey", string(out))
|
||||
}
|
||||
}
|
216
native.go
Normal file
216
native.go
Normal file
@ -0,0 +1,216 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
modwtsapi32 *windows.LazyDLL = windows.NewLazySystemDLL("wtsapi32.dll")
|
||||
modkernel32 *windows.LazyDLL = windows.NewLazySystemDLL("kernel32.dll")
|
||||
modadvapi32 *windows.LazyDLL = windows.NewLazySystemDLL("advapi32.dll")
|
||||
moduserenv *windows.LazyDLL = windows.NewLazySystemDLL("userenv.dll")
|
||||
|
||||
procWTSEnumerateSessionsW *windows.LazyProc = modwtsapi32.NewProc("WTSEnumerateSessionsW")
|
||||
procWTSGetActiveConsoleSessionId *windows.LazyProc = modkernel32.NewProc("WTSGetActiveConsoleSessionId")
|
||||
procWTSQueryUserToken *windows.LazyProc = modwtsapi32.NewProc("WTSQueryUserToken")
|
||||
procDuplicateTokenEx *windows.LazyProc = modadvapi32.NewProc("DuplicateTokenEx")
|
||||
procCreateEnvironmentBlock *windows.LazyProc = moduserenv.NewProc("CreateEnvironmentBlock")
|
||||
procCreateProcessAsUser *windows.LazyProc = modadvapi32.NewProc("CreateProcessAsUserW")
|
||||
)
|
||||
|
||||
const (
|
||||
WTS_CURRENT_SERVER_HANDLE uintptr = 0
|
||||
)
|
||||
|
||||
type WTS_CONNECTSTATE_CLASS int
|
||||
|
||||
const (
|
||||
WTSActive WTS_CONNECTSTATE_CLASS = iota
|
||||
WTSConnected
|
||||
WTSConnectQuery
|
||||
WTSShadow
|
||||
WTSDisconnected
|
||||
WTSIdle
|
||||
WTSListen
|
||||
WTSReset
|
||||
WTSDown
|
||||
WTSInit
|
||||
)
|
||||
|
||||
type SECURITY_IMPERSONATION_LEVEL int
|
||||
|
||||
const (
|
||||
SecurityAnonymous SECURITY_IMPERSONATION_LEVEL = iota
|
||||
SecurityIdentification
|
||||
SecurityImpersonation
|
||||
SecurityDelegation
|
||||
)
|
||||
|
||||
type TOKEN_TYPE int
|
||||
|
||||
const (
|
||||
TokenPrimary TOKEN_TYPE = iota + 1
|
||||
TokenImpersonazion
|
||||
)
|
||||
|
||||
type SW int
|
||||
|
||||
const (
|
||||
SW_HIDE SW = 0
|
||||
SW_SHOWNORMAL = 1
|
||||
SW_NORMAL = 1
|
||||
SW_SHOWMINIMIZED = 2
|
||||
SW_SHOWMAXIMIZED = 3
|
||||
SW_MAXIMIZE = 3
|
||||
SW_SHOWNOACTIVATE = 4
|
||||
SW_SHOW = 5
|
||||
SW_MINIMIZE = 6
|
||||
SW_SHOWMINNOACTIVE = 7
|
||||
SW_SHOWNA = 8
|
||||
SW_RESTORE = 9
|
||||
SW_SHOWDEFAULT = 10
|
||||
SW_MAX = 1
|
||||
)
|
||||
|
||||
type WTS_SESSION_INFO struct {
|
||||
SessionID windows.Handle
|
||||
WinStationName *uint16
|
||||
State WTS_CONNECTSTATE_CLASS
|
||||
}
|
||||
|
||||
const (
|
||||
CREATE_UNICODE_ENVIRONMENT uint16 = 0x00000400
|
||||
CREATE_NO_WINDOW = 0x08000000
|
||||
CREATE_NEW_CONSOLE = 0x00000010
|
||||
)
|
||||
|
||||
// GetCurrentUserSessionId will attempt to resolve
|
||||
// the session ID of the user currently active on
|
||||
// the system.
|
||||
func GetCurrentUserSessionId() (windows.Handle, error) {
|
||||
sessionList, err := WTSEnumerateSessions()
|
||||
if err != nil {
|
||||
return 0xFFFFFFFF, fmt.Errorf("get current user session token: %s", err)
|
||||
}
|
||||
|
||||
for i := range sessionList {
|
||||
if sessionList[i].State == WTSActive {
|
||||
return sessionList[i].SessionID, nil
|
||||
}
|
||||
}
|
||||
|
||||
if sessionId, _, err := procWTSGetActiveConsoleSessionId.Call(); sessionId == 0xFFFFFFFF {
|
||||
return 0xFFFFFFFF, fmt.Errorf("get current user session token: call native WTSGetActiveConsoleSessionId: %s", err)
|
||||
} else {
|
||||
return windows.Handle(sessionId), nil
|
||||
}
|
||||
}
|
||||
|
||||
// WTSEnumerateSession will call the native
|
||||
// version for Windows and parse the result
|
||||
// to a Golang friendly version
|
||||
func WTSEnumerateSessions() ([]*WTS_SESSION_INFO, error) {
|
||||
var (
|
||||
sessionInformation windows.Handle = windows.Handle(0)
|
||||
sessionCount int = 0
|
||||
sessionList []*WTS_SESSION_INFO = make([]*WTS_SESSION_INFO, 0)
|
||||
)
|
||||
|
||||
if returnCode, _, err := procWTSEnumerateSessionsW.Call(WTS_CURRENT_SERVER_HANDLE, 0, 1, uintptr(unsafe.Pointer(&sessionInformation)), uintptr(unsafe.Pointer(&sessionCount))); returnCode == 0 {
|
||||
return nil, fmt.Errorf("call native WTSEnumerateSessionsW: %s", err)
|
||||
}
|
||||
|
||||
structSize := unsafe.Sizeof(WTS_SESSION_INFO{})
|
||||
current := uintptr(sessionInformation)
|
||||
for i := 0; i < sessionCount; i++ {
|
||||
sessionList = append(sessionList, (*WTS_SESSION_INFO)(unsafe.Pointer(current)))
|
||||
current += structSize
|
||||
}
|
||||
|
||||
return sessionList, nil
|
||||
}
|
||||
|
||||
// DuplicateUserTokenFromSessionID will attempt
|
||||
// to duplicate the user token for the user logged
|
||||
// into the provided session ID
|
||||
func DuplicateUserTokenFromSessionID(sessionId windows.Handle) (windows.Token, error) {
|
||||
var (
|
||||
impersonationToken windows.Handle = 0
|
||||
userToken windows.Token = 0
|
||||
)
|
||||
|
||||
if returnCode, _, err := procWTSQueryUserToken.Call(uintptr(sessionId), uintptr(unsafe.Pointer(&impersonationToken))); returnCode == 0 {
|
||||
return 0xFFFFFFFF, fmt.Errorf("call native WTSQueryUserToken: %s", err)
|
||||
}
|
||||
|
||||
if returnCode, _, err := procDuplicateTokenEx.Call(uintptr(impersonationToken), 0, 0, uintptr(SecurityImpersonation), uintptr(TokenPrimary), uintptr(unsafe.Pointer(&userToken))); returnCode == 0 {
|
||||
return 0xFFFFFFFF, fmt.Errorf("call native DuplicateTokenEx: %s", err)
|
||||
}
|
||||
|
||||
if err := windows.CloseHandle(impersonationToken); err != nil {
|
||||
return 0xFFFFFFFF, fmt.Errorf("close windows handle used for token duplication: %s", err)
|
||||
}
|
||||
|
||||
return userToken, nil
|
||||
}
|
||||
|
||||
func StartProcessAsCurrentUser(appPath, cmdLine, workDir string) error {
|
||||
var (
|
||||
sessionId windows.Handle
|
||||
userToken windows.Token
|
||||
envInfo windows.Handle
|
||||
|
||||
startupInfo windows.StartupInfo
|
||||
processInfo windows.ProcessInformation
|
||||
|
||||
commandLine uintptr = 0
|
||||
workingDir uintptr = 0
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
if sessionId, err = GetCurrentUserSessionId(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userToken, err = DuplicateUserTokenFromSessionID(sessionId); err != nil {
|
||||
return fmt.Errorf("get duplicate user token for current user session: %s", err)
|
||||
}
|
||||
|
||||
if returnCode, _, err := procCreateEnvironmentBlock.Call(uintptr(unsafe.Pointer(&envInfo)), uintptr(userToken), 0); returnCode == 0 {
|
||||
return fmt.Errorf("create environment details for process: %s", err)
|
||||
}
|
||||
|
||||
creationFlags := CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE
|
||||
startupInfo.ShowWindow = SW_SHOW
|
||||
startupInfo.Desktop = windows.StringToUTF16Ptr("winsta0\\default")
|
||||
|
||||
if len(cmdLine) > 0 {
|
||||
commandLine = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(cmdLine)))
|
||||
}
|
||||
if len(workDir) > 0 {
|
||||
workingDir = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(workDir)))
|
||||
}
|
||||
|
||||
if returnCode, _, err := procCreateProcessAsUser.Call(
|
||||
uintptr(userToken), // hToken
|
||||
//uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(appPath))), // lpApplicationName
|
||||
uintptr(unsafe.Pointer(nil)), // lpApplicationName
|
||||
commandLine, // lpCommandLine
|
||||
0, // lpProcessAttributes
|
||||
0, // lpThreadAttributes
|
||||
0, // lpInheritHandles
|
||||
uintptr(creationFlags), // dwCreationFlags
|
||||
uintptr(envInfo), // lpEnvironment
|
||||
workingDir, // lpCurrentDirectory
|
||||
uintptr(unsafe.Pointer(&startupInfo)), // lpStartupInfo
|
||||
uintptr(unsafe.Pointer(&processInfo)), // lpProcessInformation
|
||||
); returnCode == 0 {
|
||||
return fmt.Errorf("create process as user: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user