main.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"crypto/rand"
"embed"
"flag"
"fmt"
"io/fs"
"log"
"math/big"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"context"
"github.com/josecarlosrivas/muxdeck/internal/cli"
"github.com/josecarlosrivas/muxdeck/internal/cloud"
"github.com/josecarlosrivas/muxdeck/internal/mushrun"
"github.com/josecarlosrivas/muxdeck/internal/power"
"github.com/josecarlosrivas/muxdeck/internal/remote"
"github.com/josecarlosrivas/muxdeck/internal/server"
"github.com/josecarlosrivas/muxdeck/relay"
)
//go:embed web
var webFS embed.FS
// set via -ldflags "-X main.version=..." in releases
var version = "dev"
// One binary, two jobs. A bare subcommand selects the CLI; anything else —
// no arguments, or the daemon's flags, which all start with "-" — serves.
// Shipping the client inside the server is what makes it available wherever
// muxdeck already is, which on a box you reached to look at its sessions is
// the only place it is any use.
func main() {
if len(os.Args) > 1 && os.Args[1] == "relay-server" {
relayServe(os.Args[2:])
return
}
if cli.Selected(os.Args[1:]) {
cli.Version = version
os.Exit(cli.Run(os.Args[1:]))
}
serve()
}
// relayServe runs the self-hosted relay: one binary, same as the daemon.
// TLS works exactly like the daemon's: bring cert files, or terminate in
// front (ACME on your edge).
func relayServe(args []string) {
fs := flag.NewFlagSet("relay-server", flag.ExitOnError)
addr := fs.String("addr", envOr("MUXDECK_RELAY_ADDR", "127.0.0.1:8340"), "listen address")
secret := fs.String("secret", os.Getenv("MUXDECK_RELAY_SECRET"), "shared secret the daemon dials with (required)")
tlsCert := fs.String("tls-cert", os.Getenv("MUXDECK_RELAY_TLS_CERT"), "TLS certificate file; serve HTTPS when set with -tls-key")
tlsKey := fs.String("tls-key", os.Getenv("MUXDECK_RELAY_TLS_KEY"), "TLS key file")
fs.Parse(args)
if *secret == "" {
log.Fatal("relay-server: -secret (or MUXDECK_RELAY_SECRET) is required")
}
srv := relay.NewServer(*secret)
log.Printf("muxdeck relay %s listening on %s", version, *addr)
var err error
if *tlsCert != "" || *tlsKey != "" {
if *tlsCert == "" || *tlsKey == "" {
log.Fatal("-tls-cert and -tls-key must be set together")
}
err = http.ListenAndServeTLS(*addr, *tlsCert, *tlsKey, srv)
} else {
err = http.ListenAndServe(*addr, srv)
}
log.Fatal(err)
}
func serve() {
addr := flag.String("addr", envOr("MUXDECK_ADDR", "127.0.0.1:8300"), "listen address")
tokenFlag := flag.String("token", os.Getenv("MUXDECK_TOKEN"), `access token; "auto" generates a code; empty generates one on non-loopback binds`)
noAuth := flag.Bool("no-auth", os.Getenv("MUXDECK_NO_AUTH") != "", "allow unauthenticated access on non-loopback binds")
tlsCert := flag.String("tls-cert", os.Getenv("MUXDECK_TLS_CERT"), "TLS certificate file; serve HTTPS when set with -tls-key")
tlsKey := flag.String("tls-key", os.Getenv("MUXDECK_TLS_KEY"), "TLS key file")
relayURL := flag.String("relay-url", os.Getenv("MUXDECK_RELAY_URL"), "relay tunnel URL (wss://relay.example/tunnel); session-only override of the relay config")
relayKey := flag.String("relay-key", os.Getenv("MUXDECK_RELAY_KEY"), "credential presented to the relay")
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {
fmt.Println("muxdeck", version)
return
}
static, err := fs.Sub(webFS, "web")
if err != nil {
log.Fatal(err)
}
scheme := "http"
if *tlsCert != "" || *tlsKey != "" {
if *tlsCert == "" || *tlsKey == "" {
log.Fatal("-tls-cert and -tls-key must be set together")
}
scheme = "https"
}
remotes, err := remote.Load(envOr("MUXDECK_REMOTES", remote.DefaultPath()))
if err != nil {
log.Fatal(err)
}
// ssh tunnels are child processes; kill them on the way out so they
// don't outlive the daemon across launchd/sidecar restarts.
mushruns := mushrun.New(os.Getenv("MUXDECK_MUSH_BIN"))
// The keep-awake assertion is a child too (caffeinate -w binds it to
// this pid, but a clean exit should not lean on that).
powerm, err := power.Load(envOr("MUXDECK_POWER_CONFIG", power.DefaultConfigPath()), power.Default(), log.Printf)
if err != nil {
log.Fatal(err)
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
powerm.Shutdown()
remotes.Shutdown()
mushruns.Shutdown()
os.Exit(0)
}()
relaym, err := relay.LoadManager(envOr("MUXDECK_RELAY_CONFIG", relay.DefaultConfigPath()))
if err != nil {
log.Fatal(err)
}
if *relayURL != "" {
relaym.Override(*relayURL, *relayKey)
}
// The cloud sync leaves this daemon out of its own sidebar by the name
// it was claimed under — the hostname unless relay setup was told
// otherwise (MUXDECK_NAME carries that override).
self := os.Getenv("MUXDECK_NAME")
if self == "" {
self, _ = os.Hostname()
}
cloudm, err := cloud.Load(envOr("MUXDECK_CLOUD_CONFIG", cloud.DefaultConfigPath()), remotes, self)
if err != nil {
log.Fatal(err)
}
token, generated := resolveToken(*tokenFlag, *addr, *noAuth)
if st := relaym.Status(); st.Configured && !st.Off && !st.Gated && token == "" && !*noAuth {
// A configured tunnel publishes this daemon; never come up authless.
// Same treatment a non-loopback bind gets: generate an access code.
// A gated relay is the exception: it authenticates clients itself,
// so the daemon may stay tokenless behind it.
token, generated = genCode(8), true
}
srv := server.New(static, token, generated, remotes, mushruns, relaym, cloudm, powerm)
powerm.Start(nil)
relaym.Start(srv, token != "", log.Printf)
cloudm.Start(context.Background(), log.Printf)
if st := cloudm.Status(); st.SignedIn {
log.Printf("cloud: signed in at %s; syncing machines", st.URL)
}
if st := relaym.Status(); st.Configured && !st.Off {
log.Printf("relay: dialing %s", st.URL)
}
log.Printf("muxdeck %s listening on %s", version, *addr)
for _, u := range urls(scheme, *addr) {
log.Printf(" → %s", u)
}
switch {
case generated:
log.Printf("access code: %s (set your own with -token, disable auth with -no-auth)", token)
case token != "":
log.Printf("auth: token required")
default:
log.Printf("auth: disabled (loopback bind)")
}
if scheme == "https" {
err = http.ListenAndServeTLS(*addr, *tlsCert, *tlsKey, srv)
} else {
err = http.ListenAndServe(*addr, srv)
}
log.Fatal(err)
}
// Crockford-style alphabet: no I, L, O, U, 0, 1 to avoid misreading.
const codeAlphabet = "ABCDEFGHJKMNPQRSTVWXYZ23456789"
func genCode(n int) string {
b := make([]byte, n)
for i := range b {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(codeAlphabet))))
if err != nil {
log.Fatal(err)
}
b[i] = codeAlphabet[idx.Int64()]
}
return string(b)
}
// resolveToken decides the auth material. Explicit tokens win; otherwise a
// short access code is generated whenever the bind is reachable beyond
// loopback, unless the operator opts out — secure by default.
func resolveToken(token, addr string, noAuth bool) (string, bool) {
switch {
case token == "auto":
return genCode(6), true
case token != "":
return token, false
case noAuth || isLoopback(addr):
return "", false
default:
return genCode(6), true
}
}
func isLoopback(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
// urls lists the addresses a client could plausibly reach this server at,
// expanding wildcard binds to per-interface URLs.
func urls(scheme, addr string) []string {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return []string{fmt.Sprintf("%s://%s", scheme, addr)}
}
ip := net.ParseIP(host)
if host != "" && (ip == nil || !ip.IsUnspecified()) {
return []string{fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(host, port))}
}
addrs, err := net.InterfaceAddrs()
if err != nil {
return []string{fmt.Sprintf("%s://localhost:%s", scheme, port)}
}
var out []string
for _, a := range addrs {
ipn, ok := a.(*net.IPNet)
if !ok || ipn.IP.To4() == nil || ipn.IP.IsLinkLocalUnicast() {
continue
}
out = append(out, fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(ipn.IP.String(), port)))
}
return out
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}