mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
71029cea2d
This updates all source files to use a new standard header for copyright and license declaration. Notably, copyright no longer includes a date, and we now use the standard SPDX-License-Identifier header. This commit was done almost entirely mechanically with perl, and then some minimal manual fixes. Updates #6865 Signed-off-by: Will Norris <will@tailscale.com>
199 lines
5.3 KiB
Go
199 lines
5.3 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
"software.sslmate.com/src/go-pkcs12"
|
|
"tailscale.com/atomicfile"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/version"
|
|
)
|
|
|
|
var certCmd = &ffcli.Command{
|
|
Name: "cert",
|
|
Exec: runCert,
|
|
ShortHelp: "Get TLS certs",
|
|
ShortUsage: "cert [flags] <domain>",
|
|
FlagSet: (func() *flag.FlagSet {
|
|
fs := newFlagSet("cert")
|
|
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
|
|
fs.StringVar(&certArgs.keyFile, "key-file", "", "output key file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
|
|
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
|
|
return fs
|
|
})(),
|
|
}
|
|
|
|
var certArgs struct {
|
|
certFile string
|
|
keyFile string
|
|
serve bool
|
|
}
|
|
|
|
func runCert(ctx context.Context, args []string) error {
|
|
if certArgs.serve {
|
|
s := &http.Server{
|
|
Addr: ":443",
|
|
TLSConfig: &tls.Config{
|
|
GetCertificate: localClient.GetCertificate,
|
|
},
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
|
|
if v, ok := localClient.ExpandSNIName(r.Context(), r.Host); ok {
|
|
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "<h1>Hello from Tailscale</h1>It works.")
|
|
}),
|
|
}
|
|
switch len(args) {
|
|
case 0:
|
|
// Nothing.
|
|
case 1:
|
|
s.Addr = args[0]
|
|
default:
|
|
return errors.New("too many arguments; max 1 allowed with --serve-demo (the listen address)")
|
|
}
|
|
|
|
log.Printf("running TLS server on %s ...", s.Addr)
|
|
return s.ListenAndServeTLS("", "")
|
|
}
|
|
|
|
if len(args) != 1 {
|
|
var hint bytes.Buffer
|
|
if st, err := localClient.Status(ctx); err == nil {
|
|
if st.BackendState != ipn.Running.String() {
|
|
fmt.Fprintf(&hint, "\nTailscale is not running.\n")
|
|
} else if len(st.CertDomains) == 0 {
|
|
fmt.Fprintf(&hint, "\nHTTPS cert support is not enabled/configured for your tailnet.\n")
|
|
} else if len(st.CertDomains) == 1 {
|
|
fmt.Fprintf(&hint, "\nFor domain, use %q.\n", st.CertDomains[0])
|
|
} else {
|
|
fmt.Fprintf(&hint, "\nValid domain options: %q.\n", st.CertDomains)
|
|
}
|
|
}
|
|
return fmt.Errorf("Usage: tailscale cert [flags] <domain>%s", hint.Bytes())
|
|
}
|
|
domain := args[0]
|
|
|
|
printf := func(format string, a ...any) {
|
|
printf(format, a...)
|
|
}
|
|
if certArgs.certFile == "-" || certArgs.keyFile == "-" {
|
|
printf = log.Printf
|
|
log.SetFlags(0)
|
|
}
|
|
if certArgs.certFile == "" && certArgs.keyFile == "" {
|
|
certArgs.certFile = domain + ".crt"
|
|
certArgs.keyFile = domain + ".key"
|
|
}
|
|
certPEM, keyPEM, err := localClient.CertPair(ctx, domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
needMacWarning := version.IsSandboxedMacOS()
|
|
macWarn := func() {
|
|
if !needMacWarning {
|
|
return
|
|
}
|
|
needMacWarning = false
|
|
dir := "io.tailscale.ipn.macos"
|
|
if version.IsMacSysExt() {
|
|
dir = "io.tailscale.ipn.macsys"
|
|
}
|
|
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s/Data\n", dir)
|
|
}
|
|
if certArgs.certFile != "" {
|
|
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if certArgs.certFile != "-" {
|
|
macWarn()
|
|
if certChanged {
|
|
printf("Wrote public cert to %v\n", certArgs.certFile)
|
|
} else {
|
|
printf("Public cert unchanged at %v\n", certArgs.certFile)
|
|
}
|
|
}
|
|
}
|
|
if dst := certArgs.keyFile; dst != "" {
|
|
contents := keyPEM
|
|
if isPKCS12(dst) {
|
|
var err error
|
|
contents, err = convertToPKCS12(certPEM, keyPEM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
keyChanged, err := writeIfChanged(dst, contents, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if certArgs.keyFile != "-" {
|
|
macWarn()
|
|
if keyChanged {
|
|
printf("Wrote private key to %v\n", dst)
|
|
} else {
|
|
printf("Private key unchanged at %v\n", dst)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
|
|
if filename == "-" {
|
|
Stdout.Write(contents)
|
|
return false, nil
|
|
}
|
|
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
|
|
return false, nil
|
|
}
|
|
if err := atomicfile.WriteFile(filename, contents, mode); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func isPKCS12(dst string) bool {
|
|
return strings.HasSuffix(dst, ".p12") || strings.HasSuffix(dst, ".pfx")
|
|
}
|
|
|
|
func convertToPKCS12(certPEM, keyPEM []byte) ([]byte, error) {
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var certs []*x509.Certificate
|
|
for _, c := range cert.Certificate {
|
|
cert, err := x509.ParseCertificate(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs = append(certs, cert)
|
|
}
|
|
if len(certs) == 0 {
|
|
return nil, errors.New("no certs")
|
|
}
|
|
// TODO(bradfitz): I'm not sure this is right yet. The goal was to make this
|
|
// work for https://github.com/tailscale/tailscale/issues/2928 but I'm still
|
|
// fighting Windows.
|
|
return pkcs12.Encode(rand.Reader, cert.PrivateKey, certs[0], certs[1:], "" /* no password */)
|
|
}
|