-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.go
More file actions
218 lines (185 loc) · 5.36 KB
/
main.go
File metadata and controls
218 lines (185 loc) · 5.36 KB
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package main
import (
"bufio"
"context"
"fmt"
"net"
"os"
"os/exec"
"os/signal"
"runtime/debug"
"time"
"github.com/rs/zerolog"
"github.com/barebitcoin/btc-buf/server"
)
func realMain(cfg *config) error {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(context.Canceled)
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() {
signal := <-sig
zerolog.Ctx(ctx).Info().
Stringer("signal", signal).
Msg("received signal, canceling context")
cancel(fmt.Errorf("received %s signal", signal))
}()
errs := make(chan error)
if cfg.SSH.Host != "" {
zerolog.Ctx(ctx).Info().
Msgf("setting up SSH tunnel: %d:localhost:%d -> %s",
cfg.SSH.LocalPort, cfg.SSH.RemotePort, cfg.SSH.Host,
)
if err := setupSSHTunnel(ctx, cfg.SSH, errs); err != nil {
return fmt.Errorf("setup SSH tunnel: %w", err)
}
}
clientCtx, clientCancel := context.WithTimeout(ctx, time.Second*10)
defer clientCancel()
var opts []server.Option
if cfg.AllowPrivateDescriptorsExport {
zerolog.Ctx(ctx).Info().Msg("allowing private descriptors export")
opts = append(opts, server.WithAllowPrivateDescriptorsExport())
}
bitcoind, err := server.NewBitcoind(
clientCtx, cfg.Bitcoind.Host, cfg.Bitcoind.User, cfg.Bitcoind.Pass,
opts...,
)
if err != nil {
return fmt.Errorf("new server: %w", err)
}
go func() {
if err := bitcoind.Listen(ctx, cfg.Listen); err != nil {
errs <- err
}
}()
go func() {
<-ctx.Done()
bitcoind.Shutdown(ctx)
errs <- context.Cause(ctx)
}()
return <-errs
}
func main() {
ctx := context.Background()
cfg, err := readConfig(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, "unable to read config:", err)
os.Exit(1)
}
// important: this is only usable AFTER readConfig has been called
log := zerolog.Ctx(ctx)
if info, ok := debug.ReadBuildInfo(); ok {
log.Info().
Str("go", info.GoVersion).
Str("vcs.sha", findSetting("vcs.revision", info.Settings)).
Str("vcs.modified", findSetting("vcs.modified", info.Settings)).
Msgf("starting %s", os.Args[0])
}
if err := realMain(cfg); err != nil {
log.Fatal().Err(err).Msg("main: received error")
}
log.Info().Msgf("main: exiting with 0 code")
}
func findSetting(key string, settings []debug.BuildSetting) string {
for _, setting := range settings {
if setting.Key == key {
return setting.Value
}
}
return "unknown"
}
// setupSSHTunnel creates an SSH tunnel by running the ssh command
func setupSSHTunnel(ctx context.Context, conf sshConfig, out chan error) error {
if conf.KeyFile == "" {
return fmt.Errorf("ssh: key file is required")
}
args := []string{
"-v", "-N",
"-F", "none", // don't read the default config file
"-o", "PasswordAuthentication=no", // disable password authentication
"-o", "PreferredAuthentications=publickey", // only use public key authentication
"-o", "IdentitiesOnly=yes", // only use explicitly provided keys
"-o", "ServerAliveInterval=60", // send keep-alive every 60 seconds
"-o", "ServerAliveCountMax=3", // allow 3 missed keep-alive responses before disconnecting
"-o", "TCPKeepAlive=yes", // enable TCP keep-alive
"-i", conf.KeyFile, // specify the key file to use
"-L", fmt.Sprintf("%d:localhost:%d", conf.LocalPort, conf.RemotePort),
conf.Host,
}
if conf.KnownHosts != nil {
tempFile, err := os.CreateTemp("", "")
if err != nil {
return fmt.Errorf("create temp file: %w", err)
}
for _, host := range conf.KnownHosts {
_, err := fmt.Fprintln(tempFile, host)
if err != nil {
return fmt.Errorf("write temp file: %w", err)
}
}
if err := tempFile.Close(); err != nil {
return fmt.Errorf("close temp file: %w", err)
}
args = append(args, "-o", "UserKnownHostsFile="+tempFile.Name())
}
// Build SSH command with port forwarding
// -N: Don't execute remote command (forward only)
// -L: Local port forwarding
cmd := exec.CommandContext(ctx, "ssh", args...)
// Capture stdout and stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("create stdout pipe: %w", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("create stderr pipe: %w", err)
}
// Start the SSH tunnel
if err := cmd.Start(); err != nil {
return fmt.Errorf("starting SSH tunnel: %w", err)
}
// Monitor the tunnel process in background
go func() {
if err := cmd.Wait(); err != nil {
zerolog.Ctx(ctx).Error().
Err(err).
Msg("SSH tunnel exited unexpectedly")
out <- fmt.Errorf("SSH tunnel exited unexpectedly: %w", err)
}
}()
// Log stdout in background
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
zerolog.Ctx(ctx).Debug().
Msgf("SSH tunnel stdout: %s", scanner.Text())
}
}()
// Log stderr in background
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
zerolog.Ctx(ctx).Debug().
Msgf("SSH tunnel stderr: %s", scanner.Text())
}
}()
// Wait for the tunnel to be established
for i := 0; i < 10; i++ {
if conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", conf.LocalPort)); err == nil {
if err := conn.Close(); err != nil {
return fmt.Errorf("close connection: %w", err)
}
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("wait for SSH tunnel: %w", ctx.Err())
case err := <-out:
return fmt.Errorf("setup SSH tunnel: %w", err)
case <-time.After(time.Second):
}
}
return fmt.Errorf("timeout waiting for SSH tunnel")
}