Skip to content

Commit ee9b589

Browse files
committed
Initial version of npiperelay
1 parent e24a76a commit ee9b589

File tree

6 files changed

+409
-0
lines changed

6 files changed

+409
-0
lines changed

README.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# npiperelay
2+
3+
npiperelay is a tool that allows you to access a Windows named pipe in a way
4+
that is more compatible with a variety of command-line tools. With it, you can
5+
use Windows named pipes from the Windows Subsystem for Linux (WSL).
6+
7+
For example, you can:
8+
9+
* Connect to Docker for Windows from the Linux Docker client in WSL
10+
* Connect interactively to a Hyper-V Linux VM's serial console
11+
12+
Let me know on Twitter ([@gigastarks](https://twitter.com/gigastarks)) if you come up with more interesting uses.
13+
14+
# Installation
15+
16+
Binaries for npiperelay are not currently available. You have to build from source. With Go, this is not too difficult.
17+
18+
Basic steps:
19+
20+
1. Install Go.
21+
2. Download and build the Windows binary and add it to your path.
22+
3. Install socat.
23+
24+
## Installing Go
25+
26+
To build the binary, you will need a version of [Go](https://golang.org). You can use a Windows build of Go or, as outlined here, you can use a Linux build and cross-compile the Windows binary directly from WSL.
27+
28+
## Building npiperelay.exe
29+
30+
Once you have Go installed (and your GOPATH configured), you need to download and install the tool. This is a little tricky because we are building the tool for Windows from WSL:
31+
32+
```bash
33+
$ go get -d github.com/jstarks/npiperelay
34+
$ GOOS=windows go build -o /mnt/c/Users/<myuser>/go/bin/npiperelay.exe github.com/jstarks/npiperelay
35+
```
36+
37+
In this example, we have put the binary in `/mnt/c/Users/<myuser>/go/bin`. We then need to make sure that this directory is available in the WSL path. This can be achieved either by adding C:\Users\<myuser>\go\bin to the Win32 path and restarting WSL, or by just adding the path directly in WSL via the command line or in our `.bash_profile` or `.bashrc`.
38+
39+
Or you can just symlink it into something that's already in your path:
40+
41+
```bash
42+
$ sudo ln -s /mnt/c/Users/<myuser>/go/bin/npiperelay.exe /usr/local/bin/npiperelay.exe
43+
```
44+
45+
You may be tempted to just put the real binary directly into `/usr/local/bin`, but this will not work because Windows currently cannot run binaries that exist in the Linux namespace -- they have to reside somewhere under the Windows portion of the file system.
46+
47+
## Installing socat
48+
49+
For all of the examples below, you will need the excellent `socat` tool. Your WSL distribution should
50+
have it available; install it by running
51+
52+
```bash
53+
$ sudo apt install socat
54+
```
55+
56+
or the equivalent.
57+
58+
# Usage
59+
60+
The examples below assume you have copied the contents of the `scripts` directory (from `$HOME/go/src/github.com/jstarks/npiperelay/scripts`) into your PATH somewhere. These scripts are just examples and can be modified to suit your needs.
61+
62+
## Connecting to Docker from WSL
63+
64+
This assumes you already have the Docker daemon running in Windows, e.g. because you have installed Docker for Windows. You may already have the ability to connect to this daemon from WSL via TCP, but this has security problems because any user on your machine will be able to connect. With these steps, you'll be able to limit access to privileged users.
65+
66+
Basic steps:
67+
68+
1. Start the Docker relay.
69+
2. Use the `docker` CLI as usual.
70+
71+
### Staring the Docker relay
72+
73+
For this to work, you will need to be running in an elevated WSL session, or you will need to configure Docker to allow your Windows user access without elevating.
74+
75+
You also need to be running as root within WSL, or launch the command under sudo. This is necessary because the relay will create a file /var/run/docker.sock.
76+
77+
```bash
78+
$ sudo docker-relay &
79+
```
80+
81+
### Using the docker CLI with the relay
82+
83+
At this point, ordinary `docker` commands should run fine as root. Try
84+
85+
```bash
86+
$ sudo docker info
87+
```
88+
89+
If this succeeds, then you are connected. Now try some other Docker commands:
90+
91+
```bash
92+
$ sudo docker run -it --rm microsoft/nanoserver cmd /c "Back in Windows again..."
93+
```
94+
95+
#### Running without root
96+
97+
The `docker-relay` script configured the Docker pipe to allow access by the
98+
`docker` group. To run as an ordinary user, add your WSL user to the docker
99+
group. In Ubuntu:
100+
101+
```bash
102+
$ sudo adduser <my_user> docker
103+
```
104+
105+
Then open a new WSL window to reset your group membership.
106+
107+
## Connecting to a Hyper-V Linux VM's serial console
108+
109+
If you have a Linux VM configured in Hyper-V, you may wish to use its serial
110+
port as a serial console. With npiperelay, this can be done fairly easily from
111+
the command line.
112+
113+
Basic steps:
114+
115+
1. Enable the serial port for your Linux VM.
116+
2. Configure your VM to run the console on the serial port.
117+
3. Run socat to relay between your terminal and npiperelay.
118+
119+
### Enabling the serial port
120+
121+
This is easiest to do from the command line, via the Hyper-V PowerShell cmdlets.
122+
You'll need to add your user to the Hyper-V Administrators group or run the
123+
command line elevated for this to work.
124+
125+
If you have a VM named `foo` and you want to enable the console on COM1 (/dev/ttyS0), with a named pipe name of `foo_debug_pipe`:
126+
127+
```bash
128+
$ powershell.exe Set-VMComPort foo 1 '\\.\pipe\foo_debug_pipe'
129+
```
130+
131+
### Configuring your VM to run the console on the serial port
132+
133+
Refer to your VM Linux distribution's instructions for enabling the serial console:
134+
135+
* [Ubuntu](https://help.ubuntu.com/community/SerialConsoleHowto)
136+
* [Fedora](https://docs.fedoraproject.org/f26/system-administrators-guide/kernel-module-driver-configuration/Working_with_the_GRUB_2_Boot_Loader.html#sec-GRUB_2_over_a_Serial_Console])
137+
138+
### Connecting to the serial port
139+
140+
For this step, WSL must be running elevated
141+
142+
#### Directly via socat
143+
144+
The easiest approach is to use socat to connect directly. The `vmserial-connect` script does this and even looks up the pipe name from the VM name and COM port for you:
145+
146+
```bash
147+
$ vmserial-connect foo 1
148+
<enter>
149+
Ubuntu 17.04 gigastarks-vm ttyS0
150+
151+
gigastarks-vm login:
152+
```
153+
154+
Press Ctrl-O to exit the connection and return to your shell.
155+
156+
#### Via screen
157+
158+
If you prefer to use a separate tool to connect to the device such as `screen`, then you must run a separate `socat` process to relay between the named pipe and a PTY. The `serial-relay` script does this
159+
for you with the right parameters; simply run:
160+
161+
```bash
162+
$ serial-relay //./pipe/foo_debug_pipe $HOME/foo-pty & # Starts the relay
163+
$ screen $HOME/foo-pty # Attaches to the serial terminal
164+
```
165+
166+
See the `screen` documentation (`man screen`) for more details.
167+
168+
## Custom usage
169+
170+
Take a look at the scripts for sample usage, or run `npiperelay.exe` without any parameters for parameter documentation.

npiperelay.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"io"
6+
"log"
7+
"os"
8+
"sync"
9+
"syscall"
10+
"time"
11+
12+
"golang.org/x/sys/windows"
13+
)
14+
15+
const cERROR_PIPE_NOT_CONNECTED syscall.Errno = 233
16+
17+
var (
18+
poll = flag.Bool("p", false, "poll until the the named pipe exists")
19+
closeWrite = flag.Bool("s", false, "send a 0-byte message to the pipe after EOF on stdin")
20+
closeOnEOF = flag.Bool("ep", false, "terminate on EOF reading from the pipe, even if there is more data to write")
21+
closeOnStdinEOF = flag.Bool("ei", false, "terminate on EOF reading from stdin, even if there is more data to write")
22+
verbose = flag.Bool("v", false, "verbose output on stderr")
23+
)
24+
25+
func dialPipe(p string, poll bool) (*overlappedFile, error) {
26+
p16, err := windows.UTF16FromString(p)
27+
if err != nil {
28+
return nil, err
29+
}
30+
for {
31+
h, err := windows.CreateFile(&p16[0], windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED, 0)
32+
if err == nil {
33+
return newOverlappedFile(h), nil
34+
}
35+
if poll && os.IsNotExist(err) {
36+
time.Sleep(200 * time.Millisecond)
37+
continue
38+
}
39+
return nil, &os.PathError{Path: p, Op: "open", Err: err}
40+
}
41+
}
42+
43+
func underlyingError(err error) error {
44+
if serr, ok := err.(*os.SyscallError); ok {
45+
return serr.Err
46+
}
47+
return err
48+
}
49+
50+
func main() {
51+
flag.Parse()
52+
args := flag.Args()
53+
if len(args) != 1 {
54+
flag.Usage()
55+
os.Exit(1)
56+
}
57+
58+
if *verbose {
59+
log.Println("connecting to", args[0])
60+
}
61+
62+
conn, err := dialPipe(args[0], *poll)
63+
if err != nil {
64+
log.Fatalln(err)
65+
}
66+
67+
if *verbose {
68+
log.Println("connected")
69+
}
70+
71+
var wg sync.WaitGroup
72+
wg.Add(1)
73+
go func() {
74+
_, err := io.Copy(conn, os.Stdin)
75+
if err != nil {
76+
log.Fatalln("copy from stdin to pipe failed:", err)
77+
}
78+
79+
if *verbose {
80+
log.Println("copy from stdin to pipe finished")
81+
}
82+
83+
if *closeOnStdinEOF {
84+
os.Exit(0)
85+
}
86+
87+
if *closeWrite {
88+
// A zero-byte write on a message pipe indicates that no more data
89+
// is coming.
90+
conn.Write(nil)
91+
}
92+
os.Stdin.Close()
93+
wg.Done()
94+
}()
95+
96+
_, err = io.Copy(os.Stdout, conn)
97+
if underlyingError(err) == windows.ERROR_BROKEN_PIPE || underlyingError(err) == cERROR_PIPE_NOT_CONNECTED {
98+
// The named pipe is closed and there is no more data to read. Since
99+
// named pipes are not bidirectional, there is no way for the other side
100+
// of the pipe to get more data, so do not wait for the stdin copy to
101+
// finish.
102+
if *verbose {
103+
log.Println("copy from pipe to stdout finished: pipe closed")
104+
}
105+
os.Exit(0)
106+
}
107+
108+
if err != nil {
109+
log.Fatalln("copy from pipe to stdout failed:", err)
110+
}
111+
112+
if *verbose {
113+
log.Println("copy from pipe to stdout finished")
114+
}
115+
116+
if !*closeOnEOF {
117+
os.Stdout.Close()
118+
119+
// Keep reading until we get ERROR_BROKEN_PIPE or the copy from stdin
120+
// finishes.
121+
go func() {
122+
for {
123+
_, err := conn.Read(nil)
124+
if underlyingError(err) == windows.ERROR_BROKEN_PIPE {
125+
if *verbose {
126+
log.Println("pipe closed")
127+
}
128+
os.Exit(0)
129+
} else if err != nil {
130+
log.Fatalln("pipe error:", err)
131+
}
132+
}
133+
}()
134+
135+
wg.Wait()
136+
}
137+
}

overlappedfile.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"os"
6+
"sync"
7+
"unsafe"
8+
9+
"golang.org/x/sys/windows"
10+
)
11+
12+
var getOverlappedResultFunc = windows.MustLoadDLL("kernel32.dll").MustFindProc("GetOverlappedResult")
13+
14+
type overlappedFile struct {
15+
h windows.Handle
16+
m sync.Mutex
17+
e []windows.Handle
18+
}
19+
20+
func (f *overlappedFile) getEvent() windows.Handle {
21+
f.m.Lock()
22+
if len(f.e) == 0 {
23+
f.m.Unlock()
24+
e, err := windows.CreateEvent(nil, 0, 0, nil)
25+
if err != nil {
26+
panic(err)
27+
}
28+
return e
29+
}
30+
e := f.e[len(f.e)-1]
31+
f.e = f.e[:len(f.e)-1]
32+
f.m.Unlock()
33+
return e
34+
}
35+
36+
func (f *overlappedFile) putEvent(e windows.Handle) {
37+
windows.ResetEvent(e)
38+
f.m.Lock()
39+
f.e = append(f.e, e)
40+
f.m.Unlock()
41+
}
42+
43+
func (f *overlappedFile) asyncIo(fn func(h windows.Handle, n *uint32, o *windows.Overlapped) error) (uint32, error) {
44+
o := &windows.Overlapped{}
45+
e := f.getEvent()
46+
defer f.putEvent(e)
47+
o.HEvent = e
48+
var n uint32
49+
err := fn(f.h, &n, o)
50+
if err == windows.ERROR_IO_PENDING {
51+
r, _, err := getOverlappedResultFunc.Call(uintptr(f.h), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(&n)), 1)
52+
if r == 0 {
53+
return 0, err
54+
}
55+
} else if err != nil {
56+
return 0, err
57+
}
58+
return n, nil
59+
}
60+
61+
func (f *overlappedFile) Read(b []byte) (int, error) {
62+
n, err := f.asyncIo(func(h windows.Handle, n *uint32, o *windows.Overlapped) error {
63+
return windows.ReadFile(h, b, n, o)
64+
})
65+
err = os.NewSyscallError("read", err)
66+
if err == nil && n == 0 && len(b) > 0 {
67+
err = io.EOF
68+
}
69+
return int(n), err
70+
}
71+
72+
func (f *overlappedFile) Write(b []byte) (int, error) {
73+
n, err := f.asyncIo(func(h windows.Handle, n *uint32, o *windows.Overlapped) error {
74+
return windows.WriteFile(h, b, n, o)
75+
})
76+
return int(n), os.NewSyscallError("write", err)
77+
}
78+
79+
func (f *overlappedFile) Close() error {
80+
windows.Close(f.h)
81+
f.h = 0
82+
return nil
83+
}
84+
85+
func newOverlappedFile(h windows.Handle) *overlappedFile {
86+
return &overlappedFile{h: h}
87+
}

0 commit comments

Comments
 (0)