Skip to content

Commit

Permalink
Merge pull request #88 from n1hility/winpipe
Browse files Browse the repository at this point in the history
Implement windows ssh proxy with windows pipe support
  • Loading branch information
guillaumerose authored Jan 14, 2022
2 parents 171308a + 23695cf commit e943b18
Show file tree
Hide file tree
Showing 129 changed files with 6,657 additions and 488 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,22 @@ jobs:
with:
name: qcon
path: test/qcon.log

win-sshproxy-tests:
runs-on: windows-latest # Only builds/runs on windows
timeout-minutes: 30
steps:
- uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17

- name: Build
run: go build -ldflags -H=windowsgui -o bin/win-sshproxy.exe ./cmd/win-sshproxy

- name: Test
run: go test -v .\test-win-sshproxy


5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ qemu-wrapper:
vm:
GOOS=linux CGO_ENABLED=0 go build $(LDFLAGS) -o bin/vm ./cmd/vm

# win-sshproxy is compiled as a windows GUI to support backgrounding
.PHONY: win-sshproxy
win-sshproxy:
GOOS=windows go build -ldflags -H=windowsgui -o bin/win-sshproxy.exe ./cmd/win-sshproxy

.PHONY: clean
clean:
rm -rf ./bin
Expand Down
21 changes: 19 additions & 2 deletions cmd/gvproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"syscall"
"time"

"github.com/containers/gvisor-tap-vsock/pkg/sshclient"
"github.com/containers/gvisor-tap-vsock/pkg/transport"
"github.com/containers/gvisor-tap-vsock/pkg/types"
"github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork"
Expand Down Expand Up @@ -359,7 +360,23 @@ func run(ctx context.Context, g *errgroup.Group, configuration *types.Configurat
}

for i := 0; i < len(forwardSocket); i++ {
dest := url.URL{
var (
src *url.URL
err error
)
if strings.Contains(forwardSocket[i], "://") {
src, err = url.Parse(forwardSocket[i])
if err != nil {
return err
}
} else {
src = &url.URL{
Scheme: "unix",
Path: forwardSocket[i],
}
}

dest := &url.URL{
Scheme: "ssh",
User: url.User(forwardUser[i]),
Host: sshHostPort,
Expand All @@ -368,7 +385,7 @@ func run(ctx context.Context, g *errgroup.Group, configuration *types.Configurat
j := i
g.Go(func() error {
defer os.Remove(forwardSocket[j])
forward, err := CreateSSHForward(ctx, forwardSocket[j], dest, forwardIdentify[j], vn)
forward, err := sshclient.CreateSSHForward(ctx, src, dest, forwardIdentify[j], vn)
if err != nil {
return err
}
Expand Down
73 changes: 73 additions & 0 deletions cmd/win-sshproxy/event-hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//go:build windows
// +build windows

package main

import (
"bytes"
"fmt"

"github.com/sirupsen/logrus"
"golang.org/x/sys/windows/svc/eventlog"
)

// Logrus hook that delegates to windows event log
type EventLogHook struct {
events *eventlog.Log
}

type LogFormat struct {
name string
}

func (f *LogFormat) Format(entry *logrus.Entry) ([]byte, error) {
var b *bytes.Buffer

if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}

fmt.Fprintf(b, "[%-5s] %s: %s", entry.Level.String(), f.name, entry.Message)

for key, value := range entry.Data {
fmt.Fprintf(b, " {%s = %s}", key, value)
}

b.WriteByte('\n')
return b.Bytes(), nil
}

func NewEventHook(events *eventlog.Log, name string) *EventLogHook {
logrus.SetFormatter(&LogFormat{name})
return &EventLogHook{events}
}

func (hook *EventLogHook) Fire(entry *logrus.Entry) error {
line, err := entry.String()
if err != nil {
return err
}

switch entry.Level {
case logrus.PanicLevel:
return hook.events.Error(1002, line)
case logrus.FatalLevel:
return hook.events.Error(1001, line)
case logrus.ErrorLevel:
return hook.events.Error(1000, line)
case logrus.WarnLevel:
return hook.events.Warning(1000, line)
case logrus.InfoLevel:
return hook.events.Info(1000, line)
case logrus.DebugLevel, logrus.TraceLevel:
return hook.events.Info(1001, line)
default:
return nil
}
}

func (hook *EventLogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
228 changes: 228 additions & 0 deletions cmd/win-sshproxy/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// +build windows

package main

import (
"context"
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"unsafe"

"github.com/containers/gvisor-tap-vsock/pkg/sshclient"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc/eventlog"
)

const (
ERR_BAD_ARGS = 0x000A
WM_QUIT = 0x12
)

type MSG struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt struct{ X, Y int32 }
}

var (
stateDir string
debug bool
)

func main() {
args := os.Args
if len(args) > 1 {
if args[1] == "-debug" {
debug = true
args = args[2:]
} else {
args = args[1:]
}
}

if len(args) < 5 || (len(args)-2)%3 != 0 {
alert("Usage: " + filepath.Base(os.Args[0]) + "(-debug) [name] [statedir] ([source] [dest] [identity])... \n\nThis facilty proxies windows pipes and unix sockets over ssh using the specified identity.")
os.Exit(ERR_BAD_ARGS)
}

log, err := setupLogging(args[0])
if err != nil {
os.Exit(1)
}
defer log.Close()

stateDir = args[1]

var sources, dests, identities []string
for i := 2; i < len(args)-2; i += 3 {
sources = append(sources, args[i])
dests = append(dests, args[i+1])
identities = append(identities, args[i+2])
}

ctx, cancel := context.WithCancel(context.Background())
group, ctx := errgroup.WithContext(ctx)

// Wait for a WM_QUIT message to exit
group.Go(func() error {
logrus.Debug("Starting message loop")
return messageLoop(ctx, group, cancel)
})

logrus.Debug("Setting up proxies")
setupProxies(ctx, group, sources, dests, identities)

// Wait for cmopletion (cancellation) or error
if err := group.Wait(); err != nil {
logrus.Errorf("Error occured in execution group: " + err.Error())
os.Exit(1)
}
}

func setupLogging(name string) (*eventlog.Log, error) {
// Reuse the Built-in .NET Runtime Source so that we do not
// have to provide a messaage table and modify the system
// event configuration
log, err := eventlog.Open(".NET Runtime")
if err != nil {
return nil, err
}

logrus.AddHook(NewEventHook(log, name))
if debug {
logrus.SetLevel(logrus.DebugLevel)
} else {
logrus.SetLevel(logrus.InfoLevel)
}

return log, nil
}

func messageLoop(ctx context.Context, group *errgroup.Group, cancel func()) error {
user32 := syscall.NewLazyDLL("user32.dll")
getMessage := user32.NewProc("GetMessageW")

runtime.LockOSThread() // GetMessageW relies on thread state
defer runtime.UnlockOSThread()
tid, err := saveThreadId()
if err != nil {
return err
}

// Abort the message loop thread on cancellation
group.Go(func() error {
<-ctx.Done()
terminate(tid)
return nil
})

for {
var msg = &MSG{}
ret, _, _ := getMessage.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0, 1)
if ret == 0 || int(ret) == -1 {
logrus.Info("Received QUIT notification")
cancel()
return nil
}
logrus.Infof("Unhandled message: %d", msg.message)
}
}

func setupProxies(ctx context.Context, g *errgroup.Group, sources []string, dests []string, identities []string) error {
for i := 0; i < len(sources); i++ {
var (
src *url.URL
dest *url.URL
err error
)
if strings.Contains(sources[i], "://") {
src, err = url.Parse(sources[i])
if err != nil {
return err
}
} else {
src = &url.URL{
Scheme: "unix",
Path: sources[i],
}
}

dest, err = url.Parse(dests[i])
if err != nil {
return err
}
j := i
g.Go(func() error {
forward, err := sshclient.CreateSSHForward(ctx, src, dest, identities[j], nil)
if err != nil {
return err
}
go func() {
<-ctx.Done()
// Abort pending accepts
forward.Close()
}()
loop:
for {
select {
case <-ctx.Done():
break loop
default:
// proceed
}
err := forward.AcceptAndTunnel(ctx)
if err != nil {
logrus.Debugf("Error occurred handling ssh forwarded connection: %q", err)
}
}
return nil
})
}

return nil
}

func saveThreadId() (uint32, error) {
path := filepath.Join(stateDir, "win-sshproxy.tid")
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
if err != nil {
return 0, err
}
defer file.Close()
tid := windows.GetCurrentThreadId()
fmt.Fprintf(file, "%d:%d\n", os.Getpid(), tid)
return tid, nil
}

func terminate(tid uint32) {
user32 := syscall.NewLazyDLL("user32.dll")
postMessage := user32.NewProc("PostThreadMessageW")
postMessage.Call(uintptr(tid), WM_QUIT, 0, 0)
}

// Creates an "error" style pop-up window
func alert(caption string) int {
// Error box style
format := 0x10

user32 := syscall.NewLazyDLL("user32.dll")
captionPtr, _ := syscall.UTF16PtrFromString(caption)
titlePtr, _ := syscall.UTF16PtrFromString("winpath")
ret, _, _ := user32.NewProc("MessageBoxW").Call(
uintptr(0),
uintptr(unsafe.Pointer(captionPtr)),
uintptr(unsafe.Pointer(titlePtr)),
uintptr(format))

return int(ret)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/containers/gvisor-tap-vsock
go 1.16

require (
github.com/Microsoft/go-winio v0.5.1
github.com/apparentlymart/go-cidr v1.1.0
github.com/coreos/stream-metadata-go v0.1.6
github.com/dustin/go-humanize v1.0.0
Expand All @@ -24,6 +25,7 @@ require (
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220111092808-5a964db01320
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
Expand Down
Loading

0 comments on commit e943b18

Please sign in to comment.