-
Notifications
You must be signed in to change notification settings - Fork 1
/
sshconfiggenerator.go
211 lines (172 loc) · 5.7 KB
/
sshconfiggenerator.go
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
package discoverremotemachines
// Generates SSH client configuration file with hostnames filled from Tailscale network
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"sort"
"strings"
"github.com/function61/gokit/os/osutil"
"github.com/function61/gokit/sliceutil"
"github.com/function61/tailscale-discovery/pkg/tailscalediscoveryclient"
"github.com/spf13/cobra"
)
func Entrypoint() *cobra.Command {
return &cobra.Command{
Use: "discover-remote-machines",
Short: "Generate SSH config from Tailscale devices list",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
osutil.ExitIfError(generateSSHAndPulseAudioConfigs(
osutil.CancelOnInterruptOrTerminate(nil)))
},
}
}
func generateSSHAndPulseAudioConfigs(ctx context.Context) error {
devices, err := queryDevicesFromTailscale(ctx)
if err != nil {
return err
}
myHostname, err := os.Hostname()
if err != nil {
return err
}
if err := writeSSHConfig(devices, myHostname); err != nil {
return fmt.Errorf("writeSSHConfig: %w", err)
}
if err := writePulseAudioRemotesConfig(devices, myHostname); err != nil {
return fmt.Errorf("writePulseAudioRemotesConfig: %w", err)
}
return nil
}
func writeSSHConfig(devices []tailscalediscoveryclient.Device, myHostname string) error {
discovered := []discoveredDevice{}
for _, device := range devices {
discovered = append(discovered, discoveredDevice{
ip: device.IPv4,
hostname: device.Hostname,
sshUsername: guessSSHUsernameFromHostname(device.Hostname),
})
}
// stable iteration order
sort.Slice(discovered, func(i, j int) bool { return discovered[i].hostname < discovered[j].hostname })
lines := []string{
"# this file generated by $ jsys sshconfig-generate",
"",
}
line := func(input string) { lines = append(lines, input) }
for _, node := range discovered {
if node.hostname == myHostname { // no sense in connecting to self
continue
}
/*
Host foobar.example.com
HostName foobar.example.com
User joonas
*/
line(fmt.Sprintf("Host %s", node.hostname))
line(fmt.Sprintf(" HostName %s", node.ip))
line(fmt.Sprintf(" User %s", node.sshUsername))
line("")
}
sshConfigFile := strings.Join(lines, "\n")
// currently need to remove because it's a symlink to Varasto read-only file
// (ignore error if file was not found)
if err := os.Remove("/home/joonas/.ssh/config"); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
if err := os.WriteFile("/home/joonas/.ssh/config", []byte(sshConfigFile), 0600); err != nil {
return err
}
return nil
}
type discoveredDevice struct {
ip string
hostname string
sshUsername string
}
// TODO: implement local Tailscale API query? there were troubles dialing to the unix socket from host side..
func queryDevicesFromTailscale(ctx context.Context) ([]tailscalediscoveryclient.Device, error) {
token, err := os.ReadFile("/sto/id/YBZ0O_KRNqE/token")
if err != nil {
return nil, err
}
// curl --unix-socket /var/run/tailscale/tailscaled.sock http://localhost/localapi/v0/status
// net.Dial()
// /sysroot/apps/docker/data_nobackup/overlay2/.../diff/run/tailscale/tailscaled.sock
discoveryClient := tailscalediscoveryclient.NewClient(
strings.TrimRight(string(token), "\n"),
tailscalediscoveryclient.Function61)
return discoveryClient.Devices(ctx)
}
// this is really stupid
func guessSSHUsernameFromHostname(hostname string) string {
switch {
case strings.HasSuffix(hostname, ".fn61.net"): // Flatcar machines (formerly known as CoreOS)
return "core"
case strings.HasSuffix(hostname, "pi"): // henkanpi | veikonpi
return "pi"
case hostname == "kodinautomaatio", hostname == "kotomaki":
return "pi"
case strings.HasPrefix(hostname, "oracle"): // oracle1 | oracle2 | oraclearm
return "ubuntu"
default:
return "joonas"
}
}
// adds tailscale devices with tag "audio-server" as PulseAudio remote sinks.
func writePulseAudioRemotesConfig(devices []tailscalediscoveryclient.Device, myHostname string) error {
/*
lines := []string{
"#!/usr/bin/pulseaudio -nF",
"",
".include /etc/pulse/default.pa",
"",
"# act as playback server on top of TCP",
"load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;192.168.1.0/24;100.64.0.0/10",
"",
"# list of servers we can send output to (NOTE: might not be safe to include ourselves?)",
}
*/
hostLines := []string{}
for _, device := range devices {
if !sliceutil.ContainsString(device.Tags, "tag:audio-server") { // opt-in
continue
}
// no sense in "remotely" playing to self
// (and I even might've seen PulseAudio piss itself in this situation)
if device.Hostname == myHostname {
continue
}
// load-module module-tunnel-sink sink_name=work server=tcp:100.76.39.10:4713
line := fmt.Sprintf(
"load-module module-tunnel-sink sink_name=%s server=tcp:%s:4713",
device.Hostname,
device.IPv4)
hostLines = append(hostLines, line)
}
const path = "/home/joonas/.config/pulse/default.pa"
currentContent, err := os.ReadFile(path)
if err != nil {
return err
}
// replace everything between # <remote-sinks> .... # </remote-sinks>
updatedContent := replaceBetween(string(currentContent), "# <remote-sinks>\n", "# </remote-sinks>\n", strings.Join(hostLines, "\n")+"\n")
if updatedContent == "" {
return errors.New("didn't find begin or end marker for dynamic content")
}
return os.WriteFile(path, []byte(updatedContent), 0664)
}
func replaceBetween(input string, findStart string, findEnd string, replace string) string {
startPos := strings.Index(input, findStart)
if startPos == -1 {
return ""
}
endPos := strings.Index(input[startPos:], findEnd)
if endPos == -1 {
return ""
}
return input[:startPos+len(findStart)] + replace + input[startPos + +endPos:]
}