1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package irohdial
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log/slog"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/childproc"
)
const (
MaxSocketPath = 107
MaxTicketBytes = 4096
TicketTimeout = 10 * time.Second
)
var ticketPattern = regexp.MustCompile(`^[a-z0-9]+$`)
type Config struct {
Socket string
Dumbpipe string
Log *slog.Logger
}
func (c Config) Check() error {
switch {
case !filepath.IsAbs(c.Socket):
return fmt.Errorf("irohdial: socket %q is not an absolute path", c.Socket)
case len(c.Socket) > MaxSocketPath:
return fmt.Errorf("irohdial: socket %s is %d bytes, over the %d a unix socket path takes", c.Socket, len(c.Socket), MaxSocketPath)
case !filepath.IsAbs(c.Dumbpipe):
return fmt.Errorf("irohdial: dumbpipe %q is not an absolute path", c.Dumbpipe)
}
st, err := os.Stat(c.Dumbpipe)
if err != nil {
return fmt.Errorf("irohdial: dumbpipe: %w", err)
}
if st.IsDir() || st.Mode().Perm()&0o111 == 0 {
return fmt.Errorf("irohdial: dumbpipe %s is not an executable file", c.Dumbpipe)
}
return nil
}
func CheckTicket(ticket string) error {
if len(ticket) == 0 || len(ticket) > MaxTicketBytes || !ticketPattern.MatchString(ticket) {
return errors.New("irohdial: not an iroh ticket (lowercase base32, at most 4096 bytes)")
}
return nil
}
func Run(ctx context.Context, c Config) error {
if err := c.Check(); err != nil {
return err
}
log := c.Log
if log == nil {
log = slog.New(slog.DiscardHandler)
}
if err := os.MkdirAll(filepath.Dir(c.Socket), 0o700); err != nil {
return err
}
if err := os.Remove(c.Socket); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
ln, err := net.Listen("unix", c.Socket)
if err != nil {
return err
}
if err := os.Chmod(c.Socket, 0o600); err != nil {
ln.Close()
return err
}
log.Info("the iroh dialer listens; each connection names one guest's iroh ticket", "socket", c.Socket, "dumbpipe", c.Dumbpipe)
var wg sync.WaitGroup
defer wg.Wait()
stop := make(chan struct{})
defer close(stop)
wg.Go(func() {
select {
case <-ctx.Done():
case <-stop:
}
ln.Close()
})
for {
conn, err := ln.Accept()
if err != nil {
if ctx.Err() != nil {
return nil
}
return err
}
wg.Go(func() {
serve(ctx, c, log, conn)
})
}
}
func serve(ctx context.Context, c Config, log *slog.Logger, conn net.Conn) {
defer conn.Close()
started := time.Now()
conn.SetReadDeadline(started.Add(TicketTimeout))
br := bufio.NewReaderSize(conn, MaxTicketBytes+2)
line, err := br.ReadSlice('\n')
if err != nil {
log.Warn("a connection named no ticket", "err", err.Error())
return
}
conn.SetReadDeadline(time.Time{})
ticket := strings.TrimSpace(string(line))
if err := CheckTicket(ticket); err != nil {
log.Warn("refused a connection", "err", err.Error())
return
}
dctx, cancel := context.WithCancel(ctx)
defer cancel()
cmd := exec.CommandContext(dctx, c.Dumbpipe, "connect", ticket)
stdin, err := cmd.StdinPipe()
if err != nil {
log.Warn("dumbpipe stdin", "err", err.Error())
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Warn("dumbpipe stdout", "err", err.Error())
return
}
var tail childproc.Tail
cmd.Stderr = childproc.FilterSecrets(&tail)
if err := cmd.Start(); err != nil {
log.Warn("starting dumbpipe connect", "err", err.Error())
return
}
id := ticket[:min(16, len(ticket))]
log.Info("dumbpipe connect started", "ticket", id, "pid", cmd.Process.Pid)
var copyIn sync.WaitGroup
copyIn.Go(func() {
io.Copy(stdin, br)
stdin.Close()
})
io.Copy(conn, stdout)
cancel()
err = cmd.Wait()
log.Info("dumbpipe connect ended", "ticket", id, "seconds", time.Since(started).Seconds(), "exit", fmt.Sprint(err),
"stderr", strings.TrimSpace(tail.String()))
conn.Close()
copyIn.Wait()
}