Skip to content

Commit ada919c

Browse files
committed
init
0 parents  commit ada919c

7 files changed

Lines changed: 216 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: build
2+
on:
3+
push:
4+
workflow_dispatch:
5+
jobs:
6+
build:
7+
runs-on: windows-2022
8+
steps:
9+
- name: Checkout
10+
uses: actions/checkout@v6
11+
- name: Setup Go
12+
uses: actions/setup-go@v6
13+
with:
14+
go-version: '1.26.3'
15+
- name: Build
16+
shell: pwsh
17+
run: |
18+
$version = if ($env:GITHUB_REF -like "refs/tags/v*") {
19+
$env:GITHUB_REF -replace "refs/tags/v", ""
20+
} else {
21+
"0.0.0-dev"
22+
}
23+
$revision = $env:GITHUB_SHA
24+
go build -ldflags="-s -w -X main.version=$version -X main.revision=$revision"
25+
- name: Create artifact
26+
shell: pwsh
27+
run: |
28+
&"C:\Program Files\7-Zip\7z.exe" `
29+
a `
30+
-r `
31+
-tzip `
32+
-mx=9 `
33+
use-ssh-over-hyper-v-socket.zip `
34+
use-ssh-over-hyper-v-socket.exe
35+
- name: Upload artifact
36+
uses: actions/upload-artifact@v7
37+
with:
38+
name: use-ssh-over-hyper-v-socket
39+
path: use-ssh-over-hyper-v-socket.zip
40+
release:
41+
if: startsWith(github.ref, 'refs/tags/v')
42+
name: Release
43+
runs-on: ubuntu-24.04
44+
needs:
45+
- build
46+
permissions:
47+
contents: write
48+
steps:
49+
- name: Download artifacts
50+
uses: actions/download-artifact@v8
51+
- name: Release
52+
uses: ncipollo/release-action@v1
53+
with:
54+
prerelease: ${{ contains(github.ref_name, '-') }}
55+
artifacts: '*.zip'
56+
token: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.exe
2+
*.zip

.vscode/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"cSpell.words": [
3+
"hvsock",
4+
"vmid",
5+
"vsock",
6+
"winio"
7+
]
8+
}

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# About
2+
3+
[![build](https://github.com/rgl/use-ssh-over-hyper-v-socket/actions/workflows/build.yml/badge.svg)](https://github.com/rgl/use-ssh-over-hyper-v-socket/actions/workflows/build.yml)
4+
5+
An example Go application that executes a command using SSH over a Hyper-V VMBus Socket (aka vsock, aka hvsock).
6+
7+
## Usage
8+
9+
Start a Linux based Hyper-V Virtual Machine named `vm1` (e.g. like in https://github.com/rgl/ansible-hyperv-ubuntu-vm).
10+
11+
**NB** The `sshd` daemon running inside the Virtual Machine must be listening in the `vsock::22` address (like configured in https://github.com/rgl/ubuntu-vagrant).
12+
13+
**NB** A Hyper-V Socket can only be accessed by a user in the `Administrators` or `Hyper-V Administrators` group.
14+
15+
In a PowerShell session, execute:
16+
17+
```powershell
18+
.\use-ssh-over-hyper-v-socket.exe `
19+
-username vagrant `
20+
-password vagrant `
21+
-vmid ((Get-VM vm1).ID) `
22+
-command 'ps -efww --forest'
23+
```

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module use-ssh-over-hyper-v-socket
2+
3+
go 1.26.3
4+
5+
require (
6+
github.com/Microsoft/go-winio v0.6.2
7+
golang.org/x/crypto v0.51.0
8+
)
9+
10+
require golang.org/x/sys v0.44.0 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
2+
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
3+
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
4+
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
5+
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
6+
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
7+
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
8+
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=

main.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"os"
10+
11+
"github.com/Microsoft/go-winio"
12+
"github.com/Microsoft/go-winio/pkg/guid"
13+
"golang.org/x/crypto/ssh"
14+
)
15+
16+
var (
17+
versionFlag = flag.Bool("version", false, "show version")
18+
vmidFlag = flag.String("vmid", "", "hyper-v VM ID (GUID)")
19+
sshUsernameFlag = flag.String("username", "vagrant", "ssh username")
20+
sshPasswordFlag = flag.String("password", "", "ssh password")
21+
sshPortFlag = flag.Uint("port", 22, "ssh port")
22+
commandStdinFlag = flag.String("stdin", "", "data to pass into the command stdin")
23+
commandFlag = flag.String("command", "ps -efww --forest", "command to execute")
24+
version = "0.0.0-dev"
25+
revision = "0000000000000000000000000000000000000000"
26+
)
27+
28+
func main() {
29+
log.SetOutput(os.Stdout) // for not disturbing PowerShell...
30+
31+
flag.Parse()
32+
33+
if *versionFlag {
34+
fmt.Printf("%s+%s\n", version, revision)
35+
return
36+
}
37+
38+
log.Printf("Executing the %s command...", *commandFlag)
39+
40+
exitCode, output, err := executeCommand(*commandStdinFlag, *commandFlag)
41+
if err != nil {
42+
log.Fatalf("failed to execute command: %v", err)
43+
}
44+
45+
log.Printf("Command ended with exit code %d and output:\n%s", exitCode, output)
46+
47+
os.Exit(exitCode)
48+
}
49+
50+
func executeCommand(stdin string, command string) (int, string, error) {
51+
config := &ssh.ClientConfig{
52+
User: *sshUsernameFlag,
53+
Auth: []ssh.AuthMethod{},
54+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
55+
}
56+
57+
if *sshPasswordFlag != "" {
58+
config.Auth = append(config.Auth, ssh.Password(*sshPasswordFlag))
59+
}
60+
61+
log.Printf("Connecting to %s on port %d...", *vmidFlag, *sshPortFlag)
62+
63+
vmid, err := guid.FromString(*vmidFlag)
64+
if err != nil {
65+
log.Fatalf("Failed to parse VM ID: %v", err)
66+
}
67+
addr := &winio.HvsockAddr{
68+
VMID: vmid,
69+
ServiceID: winio.VsockServiceID(uint32(*sshPortFlag)),
70+
}
71+
conn, err := winio.Dial(context.Background(), addr)
72+
if err != nil {
73+
log.Fatalf("Failed to dial: %v", err)
74+
}
75+
c, chans, reqs, err := ssh.NewClientConn(conn, addr.String(), config)
76+
if err != nil {
77+
log.Fatalf("Failed to create connection: %v", err)
78+
}
79+
client := ssh.NewClient(c, chans, reqs)
80+
defer client.Close()
81+
82+
log.Printf("Connected from %s (%s) to %s (%s)",
83+
client.LocalAddr(),
84+
client.ClientVersion(),
85+
client.RemoteAddr(),
86+
client.ServerVersion())
87+
88+
log.Printf("Creating SSH session to %s...", addr)
89+
90+
session, err := client.NewSession()
91+
if err != nil {
92+
return -1, "", fmt.Errorf("failed to create session: %w", err)
93+
}
94+
defer session.Close()
95+
96+
if stdin != "" {
97+
session.Stdin = bytes.NewBufferString(stdin)
98+
}
99+
100+
output, err := session.CombinedOutput(command)
101+
if err != nil {
102+
if e, ok := err.(*ssh.ExitError); ok {
103+
return e.ExitStatus(), string(output), nil
104+
}
105+
return -1, "", fmt.Errorf("failed to run command: %w", err)
106+
}
107+
108+
return 0, string(output), nil
109+
}

0 commit comments

Comments
 (0)