Skip to content

Commit 72f8ef5

Browse files
committed
Code dedup, add to readme
1 parent a0d2cc9 commit 72f8ef5

File tree

5 files changed

+200
-189
lines changed

5 files changed

+200
-189
lines changed

README.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
[![Go Reference](https://pkg.go.dev/badge/github.com/coder/wush.svg)](https://pkg.go.dev/github.com/coder/wush)
44

5-
`wush` is a command line tool that lets you easily transfer
6-
files and open shells over a peer-to-peer wireguard connection. It's similar to [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) but doesn't require you to
7-
set up or trust a relay server.
5+
`wush` is a command line tool that lets you easily transfer files and open
6+
shells over a peer-to-peer wireguard connection. It's similar to
7+
[magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) but doesn't
8+
require you to set up or trust a relay server for authentication.
89

910
## Basic Usage
1011

@@ -47,4 +48,29 @@ coder@colin:~$
4748
4849
## Technical Details
4950
50-
...
51+
`wush` doesn't require you to trust any 3rd party authentication or relay
52+
servers, instead using x25519 keys to authenticate incoming connections. Auth
53+
keys generated by `wush receive` are separated into a couple parts:
54+
55+
```text
56+
+---------------------+------------------+---------------------------+----------------------------+
57+
| UDP Address (1-19B) | DERP Region (2B) | Server Public Key (32B) | Sender Private Key (32B) |
58+
+---------------------+------------------+---------------------------+----------------------------+
59+
| 203.128.89.74:57321 | 21 | QPGoX1GY......488YNqsyWM= | o/FXVnOn.....llrKg5bqxlgY= |
60+
+---------------------+------------------+---------------------------+----------------------------+
61+
```
62+
63+
Senders and receivers communicate over what we call an "overlay". An overlay
64+
runs over one of two currently implemented mediums; UDP or DERP. Each message is
65+
encrypted with the sender's private key.
66+
67+
**UDP**: The receiver creates a NAT holepunch to allow senders to connect
68+
directly. Wireguard nodes are exchanged peer-to-peer.
69+
70+
**DERP**: The receiver connects to the closet DERP relay server. Wireguard nodes
71+
are exchanged through the relay.
72+
73+
In both cases auth is handled the same way. The receiver will only accept
74+
messages encrypted from the sender's private key, to the server's public key.
75+
76+
## Why create another file transfer tool?

cmd/wush/cp.go

Lines changed: 94 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,72 +18,105 @@ import (
1818
"github.com/coder/wush/tsserver"
1919
"github.com/schollz/progressbar/v3"
2020
"tailscale.com/net/netns"
21+
"tailscale.com/types/ptr"
2122
)
2223

23-
func cpCmd() *serpent.Command {
24-
var (
25-
authID string
26-
waitP2P bool
27-
stunAddrOverride string
28-
stunAddrOverrideIP netip.Addr
29-
)
30-
return &serpent.Command{
31-
Use: "cp <file>",
32-
Short: "Transfer files.",
33-
Long: "Transfer files to a " + cliui.Code("wush") + " peer. ",
34-
Middleware: serpent.Chain(
35-
serpent.RequireNArgs(1),
36-
),
37-
Handler: func(inv *serpent.Invocation) error {
38-
ctx := inv.Context()
39-
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
40-
logF := func(str string, args ...any) {
41-
fmt.Fprintf(inv.Stderr, str+"\n", args...)
24+
func initLogger(verbose, quiet *bool, slogger *slog.Logger, logf *func(str string, args ...any)) serpent.MiddlewareFunc {
25+
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
26+
return func(i *serpent.Invocation) error {
27+
if *verbose {
28+
*slogger = *slog.New(slog.NewTextHandler(i.Stderr, nil))
29+
} else {
30+
*slogger = *slog.New(slog.NewTextHandler(io.Discard, nil))
4231
}
4332

44-
if authID == "" {
33+
*logf = func(str string, args ...any) {
34+
if !*quiet {
35+
fmt.Fprintf(i.Stderr, str+"\n", args...)
36+
}
37+
}
38+
39+
return next(i)
40+
}
41+
}
42+
}
43+
44+
func initAuth(authFlag *string, ca *overlay.ClientAuth) serpent.MiddlewareFunc {
45+
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
46+
return func(i *serpent.Invocation) error {
47+
if *authFlag == "" {
4548
err := huh.NewInput().
4649
Title("Enter your Auth ID:").
47-
Value(&authID).
50+
Value(authFlag).
4851
Run()
4952
if err != nil {
5053
return fmt.Errorf("get auth id: %w", err)
5154
}
5255
}
5356

54-
dm, err := tsserver.DERPMapTailscale(ctx)
57+
err := ca.Parse(*authFlag)
58+
if err != nil {
59+
return fmt.Errorf("parse auth key: %w", err)
60+
}
61+
62+
return next(i)
63+
}
64+
}
65+
}
66+
67+
func sendOverlayMW(opts *sendOverlayOpts, send **overlay.Send, logger *slog.Logger, logf *func(str string, args ...any)) serpent.MiddlewareFunc {
68+
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
69+
return func(i *serpent.Invocation) error {
70+
dm, err := tsserver.DERPMapTailscale(i.Context())
5571
if err != nil {
5672
return err
5773
}
5874

59-
if stunAddrOverride != "" {
60-
stunAddrOverrideIP, err = netip.ParseAddr(stunAddrOverride)
75+
newSend := overlay.NewSendOverlay(logger, dm)
76+
newSend.Auth = opts.clientAuth
77+
if opts.stunAddrOverride != "" {
78+
newSend.STUNIPOverride, err = netip.ParseAddr(opts.stunAddrOverride)
6179
if err != nil {
6280
return fmt.Errorf("parse stun addr override: %w", err)
6381
}
6482
}
6583

66-
send := overlay.NewSendOverlay(logger, dm)
67-
send.STUNIPOverride = stunAddrOverrideIP
84+
newSend.Auth.PrintDebug(*logf, dm)
6885

69-
err = send.Auth.Parse(authID)
70-
if err != nil {
71-
return fmt.Errorf("parse auth key: %w", err)
72-
}
86+
*send = newSend
87+
return next(i)
88+
}
89+
}
90+
}
7391

74-
logF("Auth information:")
75-
stunStr := send.Auth.ReceiverStunAddr.String()
76-
if !send.Auth.ReceiverStunAddr.IsValid() {
77-
stunStr = "Disabled"
78-
}
79-
logF("\t> Server overlay STUN address: %s", cliui.Code(stunStr))
80-
derpStr := "Disabled"
81-
if send.Auth.ReceiverDERPRegionID > 0 {
82-
derpStr = dm.Regions[int(send.Auth.ReceiverDERPRegionID)].RegionName
83-
}
84-
logF("\t> Server overlay DERP home: %s", cliui.Code(derpStr))
85-
logF("\t> Server overlay public key: %s", cliui.Code(send.Auth.ReceiverPublicKey.ShortString()))
86-
logF("\t> Server overlay auth key: %s", cliui.Code(send.Auth.OverlayPrivateKey.Public().ShortString()))
92+
type sendOverlayOpts struct {
93+
authKey string
94+
clientAuth overlay.ClientAuth
95+
waitP2P bool
96+
stunAddrOverride string
97+
}
98+
99+
func cpCmd() *serpent.Command {
100+
var (
101+
verbose bool
102+
logger = new(slog.Logger)
103+
logf = func(str string, args ...any) {}
104+
105+
overlayOpts = new(sendOverlayOpts)
106+
send = new(overlay.Send)
107+
)
108+
return &serpent.Command{
109+
Use: "cp <file>",
110+
Short: "Transfer files.",
111+
Long: "Transfer files to a " + cliui.Code("wush") + " peer. ",
112+
Middleware: serpent.Chain(
113+
serpent.RequireNArgs(1),
114+
initLogger(&verbose, ptr.To(false), logger, &logf),
115+
initAuth(&overlayOpts.authKey, &overlayOpts.clientAuth),
116+
sendOverlayMW(overlayOpts, &send, logger, &logf),
117+
),
118+
Handler: func(inv *serpent.Invocation) error {
119+
ctx := inv.Context()
87120

88121
s, err := tsserver.NewServer(ctx, logger, send)
89122
if err != nil {
@@ -107,22 +140,22 @@ func cpCmd() *serpent.Command {
107140
ts.Logf = func(string, ...any) {}
108141
ts.UserLogf = func(string, ...any) {}
109142

110-
logF("Bringing Wireguard up..")
143+
logf("Bringing Wireguard up..")
111144
ts.Up(ctx)
112-
logF("Wireguard is ready!")
145+
logf("Wireguard is ready!")
113146

114147
lc, err := ts.LocalClient()
115148
if err != nil {
116149
return err
117150
}
118151

119-
ip, err := waitUntilHasPeerHasIP(ctx, logF, lc)
152+
ip, err := waitUntilHasPeerHasIP(ctx, logf, lc)
120153
if err != nil {
121154
return err
122155
}
123156

124-
if waitP2P {
125-
err := waitUntilHasP2P(ctx, logF, lc)
157+
if overlayOpts.waitP2P {
158+
err := waitUntilHasP2P(ctx, logf, lc)
126159
if err != nil {
127160
return err
128161
}
@@ -172,22 +205,29 @@ func cpCmd() *serpent.Command {
172205
},
173206
Options: []serpent.Option{
174207
{
175-
Flag: "auth-id",
176-
Env: "WUSH_AUTH_ID",
177-
Description: "The auth id returned by " + cliui.Code("wush receive") + ". If not provided, it will be asked for on startup.",
208+
Flag: "auth-key",
209+
Env: "WUSH_AUTH_key",
210+
Description: "The auth key returned by " + cliui.Code("wush receive") + ". If not provided, it will be asked for on startup.",
178211
Default: "",
179-
Value: serpent.StringOf(&authID),
212+
Value: serpent.StringOf(&overlayOpts.authKey),
180213
},
181214
{
182215
Flag: "stun-ip-override",
183216
Default: "",
184-
Value: serpent.StringOf(&stunAddrOverride),
217+
Value: serpent.StringOf(&overlayOpts.stunAddrOverride),
185218
},
186219
{
187220
Flag: "wait-p2p",
188221
Description: "Waits for the connection to be p2p.",
189222
Default: "false",
190-
Value: serpent.BoolOf(&waitP2P),
223+
Value: serpent.BoolOf(&overlayOpts.waitP2P),
224+
},
225+
{
226+
Flag: "verbose",
227+
FlagShorthand: "v",
228+
Description: "Enable verbose logging.",
229+
Default: "false",
230+
Value: serpent.BoolOf(&verbose),
191231
},
192232
},
193233
}

cmd/wush/rsync.go

Lines changed: 27 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ package main
22

33
import (
44
"fmt"
5-
"io"
65
"log/slog"
7-
"net/netip"
86
"os"
97
"os/exec"
108
"strings"
119

12-
"github.com/charmbracelet/huh"
10+
"tailscale.com/types/ptr"
1311

1412
"github.com/coder/serpent"
1513
"github.com/coder/wush/cliui"
@@ -19,11 +17,12 @@ import (
1917

2018
func rsyncCmd() *serpent.Command {
2119
var (
22-
authID string
23-
overlayTransport string
24-
stunAddrOverride string
25-
stunAddrOverrideIP netip.Addr
26-
sshStdio bool
20+
verbose bool
21+
logger = new(slog.Logger)
22+
logf = func(str string, args ...any) {}
23+
24+
overlayOpts = new(sendOverlayOpts)
25+
send = new(overlay.Send)
2726
)
2827
return &serpent.Command{
2928
Use: "rsync [flags] -- [rsync args]",
@@ -32,62 +31,27 @@ func rsyncCmd() *serpent.Command {
3231
"Use " + cliui.Code("wush receive") + " on the computer you would like to connect to." +
3332
"\n\n" +
3433
"Example: " + "wush rsync -- --progress --stats -avz --human-readable /local/path :/remote/path",
34+
Middleware: serpent.Chain(
35+
initLogger(&verbose, ptr.To(false), logger, &logf),
36+
initAuth(&overlayOpts.authKey, &overlayOpts.clientAuth),
37+
),
3538
Handler: func(inv *serpent.Invocation) error {
3639
ctx := inv.Context()
37-
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
38-
39-
if authID == "" {
40-
err := huh.NewInput().
41-
Title("Enter your Auth ID:").
42-
Value(&authID).
43-
Run()
44-
if err != nil {
45-
return fmt.Errorf("get auth id: %w", err)
46-
}
47-
}
4840

49-
dm, err := tsserver.DERPMapTailscale(ctx)
41+
dm, err := tsserver.DERPMapTailscale(inv.Context())
5042
if err != nil {
5143
return err
5244
}
53-
54-
if stunAddrOverride != "" {
55-
stunAddrOverrideIP, err = netip.ParseAddr(stunAddrOverride)
56-
if err != nil {
57-
return fmt.Errorf("parse stun addr override: %w", err)
58-
}
59-
}
60-
61-
send := overlay.NewSendOverlay(logger, dm)
62-
send.STUNIPOverride = stunAddrOverrideIP
63-
64-
err = send.Auth.Parse(authID)
65-
if err != nil {
66-
return fmt.Errorf("parse auth key: %w", err)
67-
}
68-
69-
fmt.Println("Auth information:")
70-
stunStr := send.Auth.ReceiverStunAddr.String()
71-
if !send.Auth.ReceiverStunAddr.IsValid() {
72-
stunStr = "Disabled"
73-
}
74-
fmt.Println("\t> Server overlay STUN address:", cliui.Code(stunStr))
75-
derpStr := "Disabled"
76-
if send.Auth.ReceiverDERPRegionID > 0 {
77-
derpStr = dm.Regions[int(send.Auth.ReceiverDERPRegionID)].RegionName
78-
}
79-
fmt.Println("\t> Server overlay DERP home: ", cliui.Code(derpStr))
80-
fmt.Println("\t> Server overlay public key: ", cliui.Code(send.Auth.ReceiverPublicKey.ShortString()))
81-
fmt.Println("\t> Server overlay auth key: ", cliui.Code(send.Auth.OverlayPrivateKey.Public().ShortString()))
45+
overlayOpts.clientAuth.PrintDebug(logf, dm)
8246

8347
progPath := os.Args[0]
8448
args := []string{
8549
"-c",
86-
fmt.Sprintf(`rsync -e "%s ssh --auth-key %s --stdio --" %s`,
50+
fmt.Sprintf(`rsync -e "%s ssh --auth-key %s --quiet --" %s`,
8751
progPath, send.Auth.AuthKey(), strings.Join(inv.Args, " "),
8852
),
8953
}
90-
fmt.Println("Running: rsync", strings.Join(args, " "))
54+
fmt.Println("Running: rsync", strings.Join(inv.Args, " "))
9155
cmd := exec.CommandContext(ctx, "sh", args...)
9256
cmd.Stdin = inv.Stdin
9357
cmd.Stdout = inv.Stdout
@@ -101,24 +65,25 @@ func rsyncCmd() *serpent.Command {
10165
Env: "WUSH_AUTH_KEY",
10266
Description: "The auth key returned by " + cliui.Code("wush receive") + ". If not provided, it will be asked for on startup.",
10367
Default: "",
104-
Value: serpent.StringOf(&authID),
105-
},
106-
{
107-
Flag: "overlay-transport",
108-
Description: "The transport to use on the overlay. The overlay is used to exchange Wireguard nodes between peers. In DERP mode, nodes are exchanged over public Tailscale DERPs, while STUN mode sends nodes directly over UDP.",
109-
Default: "derp",
110-
Value: serpent.EnumOf(&overlayTransport, "derp", "stun"),
68+
Value: serpent.StringOf(&overlayOpts.authKey),
11169
},
11270
{
11371
Flag: "stun-ip-override",
11472
Default: "",
115-
Value: serpent.StringOf(&stunAddrOverride),
73+
Value: serpent.StringOf(&overlayOpts.stunAddrOverride),
11674
},
11775
{
118-
Flag: "stdio",
119-
Description: "Run SSH over stdin/stdout. This allows wush to be used as a transport for other programs, like rsync or regular ssh.",
76+
Flag: "wait-p2p",
77+
Description: "Waits for the connection to be p2p.",
12078
Default: "false",
121-
Value: serpent.BoolOf(&sshStdio),
79+
Value: serpent.BoolOf(&overlayOpts.waitP2P),
80+
},
81+
{
82+
Flag: "verbose",
83+
FlagShorthand: "v",
84+
Description: "Enable verbose logging.",
85+
Default: "false",
86+
Value: serpent.BoolOf(&verbose),
12287
},
12388
},
12489
}

0 commit comments

Comments
 (0)