package aperture import ( "crypto/tls" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" "time" "github.com/coreos/etcd/clientv3" "github.com/lightninglabs/aperture/auth" "github.com/lightninglabs/aperture/mint" "github.com/lightninglabs/aperture/proxy" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/tor" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "gopkg.in/yaml.v2" ) const ( // topLevelKey is the top level key for an etcd cluster where we'll // store all LSAT proxy related data. topLevelKey = "lsat/proxy" // etcdKeyDelimeter is the delimeter we'll use for all etcd keys to // represent a path-like structure. etcdKeyDelimeter = "/" ) var ( // http2TLSCipherSuites is the list of cipher suites we allow the server // to use. This list removes a CBC cipher from the list used in lnd's // cert package because the underlying HTTP/2 library treats it as a bad // cipher, according to https://tools.ietf.org/html/rfc7540#appendix-A // (also see golang.org/x/net/http2/ciphers.go). http2TLSCipherSuites = []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, } ) // Main is the true entrypoint of Kirin. func Main() { // TODO: Prevent from running twice. err := start() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // start sets up the proxy server and runs it. This function blocks until a // shutdown signal is received. func start() error { // First, parse configuration file and set up logging. configFile := filepath.Join(apertureDataDir, defaultConfigFilename) cfg, err := getConfig(configFile) if err != nil { return fmt.Errorf("unable to parse config file: %v", err) } err = setupLogging(cfg) if err != nil { return fmt.Errorf("unable to set up logging: %v", err) } // Initialize our etcd client. etcdClient, err := clientv3.New(clientv3.Config{ Endpoints: []string{cfg.Etcd.Host}, DialTimeout: 5 * time.Second, Username: cfg.Etcd.User, Password: cfg.Etcd.Password, }) if err != nil { return fmt.Errorf("unable to connect to etcd: %v", err) } // Create the proxy and connect it to lnd. genInvoiceReq := func() (*lnrpc.Invoice, error) { return &lnrpc.Invoice{ Memo: "LSAT", Value: 1, }, nil } servicesProxy, err := createProxy(cfg, genInvoiceReq, etcdClient) if err != nil { return err } handler := http.HandlerFunc(servicesProxy.ServeHTTP) httpsServer := &http.Server{ Addr: cfg.ListenAddr, Handler: handler, } // Create TLS certificates. switch { // When using autocert, we set a TLSConfig on the server so the key and // cert file we pass in are ignored and don't need to exist. case cfg.AutoCert: serverName := cfg.ServerName if serverName == "" { return fmt.Errorf("servername option is required for " + "secure operation") } certDir := filepath.Join(apertureDataDir, "autocert") log.Infof("Configuring autocert for server %v with cache dir "+ "%v", serverName, certDir) manager := autocert.Manager{ Cache: autocert.DirCache(certDir), Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(serverName), } go func() { err := http.ListenAndServe( ":http", manager.HTTPHandler(nil), ) if err != nil { log.Errorf("autocert http: %v", err) } }() httpsServer.TLSConfig = &tls.Config{ GetCertificate: manager.GetCertificate, CipherSuites: http2TLSCipherSuites, MinVersion: tls.VersionTLS12, } // If we're not using autocert, we want to create self-signed TLS certs // and save them at the specified location (if they don't already // exist). default: tlsKeyFile := filepath.Join(apertureDataDir, defaultTLSKeyFilename) tlsCertFile := filepath.Join(apertureDataDir, defaultTLSCertFilename) if !fileExists(tlsCertFile) && !fileExists(tlsKeyFile) { log.Infof("Generating TLS certificates...") err := cert.GenCertPair( "aperture autogenerated cert", tlsCertFile, tlsKeyFile, nil, nil, cert.DefaultAutogenValidity, ) if err != nil { return err } log.Infof("Done generating TLS certificates") } // Load the certs now so we can create a complete TLS config. certData, _, err := cert.LoadCert(tlsCertFile, tlsKeyFile) if err != nil { return err } httpsServer.TLSConfig = &tls.Config{ Certificates: []tls.Certificate{certData}, CipherSuites: http2TLSCipherSuites, MinVersion: tls.VersionTLS12, } } // The ListenAndServeTLS below will block until shut down or an error // occurs. So we can just defer a cleanup function here that will close // everything on shutdown. defer cleanup(etcdClient, httpsServer) // Finally start the server. log.Infof("Starting the server, listening on %s.", cfg.ListenAddr) errChan := make(chan error) go func() { // The httpsServer.TLSConfig contains certificates at this point // so we don't need to pass in certificate and key file names. errChan <- httpsServer.ListenAndServeTLS("", "") }() // If we need to listen over Tor as well, we'll set up the onion // services now. We're not able to use TLS for onion services since they // can't be verified, so we'll spin up an additional HTTP/2 server // _without_ TLS that is not exposed to the outside world. This server // will only be reached through the onion services, which already // provide encryption, so running this additional HTTP server should be // relatively safe. if cfg.Tor.V2 || cfg.Tor.V3 { torController, err := initTorListener(cfg, etcdClient) if err != nil { return err } defer func() { _ = torController.Stop() }() httpServer := &http.Server{ Addr: fmt.Sprintf("localhost:%d", cfg.Tor.ListenPort), Handler: h2c.NewHandler(handler, &http2.Server{}), } go func() { errChan <- httpServer.ListenAndServe() }() defer httpServer.Close() } return <-errChan } // fileExists reports whether the named file or directory exists. // This function is taken from https://github.com/btcsuite/btcd func fileExists(name string) bool { if _, err := os.Stat(name); err != nil { if os.IsNotExist(err) { return false } } return true } // getConfig loads and parses the configuration file then checks it for valid // content. func getConfig(configFile string) (*config, error) { cfg := &config{} b, err := ioutil.ReadFile(configFile) if err != nil { return nil, err } err = yaml.Unmarshal(b, cfg) if err != nil { return nil, err } // Then check the configuration that we got from the config file, all // required values need to be set at this point. if cfg.ListenAddr == "" { return nil, fmt.Errorf("missing listen address for server") } return cfg, nil } // setupLogging parses the debug level and initializes the log file rotator. func setupLogging(cfg *config) error { if cfg.DebugLevel == "" { cfg.DebugLevel = defaultLogLevel } // Now initialize the logger and set the log level. logFile := filepath.Join(apertureDataDir, defaultLogFilename) err := logWriter.InitLogRotator( logFile, defaultMaxLogFileSize, defaultMaxLogFiles, ) if err != nil { return err } return build.ParseAndSetDebugLevels(cfg.DebugLevel, logWriter) } // initTorListener initiates a Tor controller instance with the Tor server // specified in the config. Onion services will be created over which the proxy // can be reached at. func initTorListener(cfg *config, etcd *clientv3.Client) (*tor.Controller, error) { // Establish a controller connection with the backing Tor server and // proceed to create the requested onion services. onionCfg := tor.AddOnionConfig{ VirtualPort: int(cfg.Tor.VirtualPort), TargetPorts: []int{int(cfg.Tor.ListenPort)}, Store: newOnionStore(etcd), } torController := tor.NewController(cfg.Tor.Control, "", "") if err := torController.Start(); err != nil { return nil, err } if cfg.Tor.V2 { onionCfg.Type = tor.V2 addr, err := torController.AddOnion(onionCfg) if err != nil { return nil, err } log.Infof("Listening over Tor on %v", addr) } if cfg.Tor.V3 { onionCfg.Type = tor.V3 addr, err := torController.AddOnion(onionCfg) if err != nil { return nil, err } log.Infof("Listening over Tor on %v", addr) } return torController, nil } // createProxy creates the proxy with all the services it needs. func createProxy(cfg *config, genInvoiceReq InvoiceRequestGenerator, etcdClient *clientv3.Client) (*proxy.Proxy, error) { challenger, err := NewLndChallenger(cfg.Authenticator, genInvoiceReq) if err != nil { return nil, err } minter := mint.New(&mint.Config{ Challenger: challenger, Secrets: newSecretStore(etcdClient), ServiceLimiter: newStaticServiceLimiter(cfg.Services), }) authenticator := auth.NewLsatAuthenticator(minter) return proxy.New(authenticator, cfg.Services, cfg.StaticRoot) } // cleanup closes the given server and shuts down the log rotator. func cleanup(etcdClient io.Closer, server io.Closer) { if err := etcdClient.Close(); err != nil { log.Errorf("Error terminating etcd client: %v", err) } err := server.Close() if err != nil { log.Errorf("Error closing server: %v", err) } log.Info("Shutdown complete") err = logWriter.Close() if err != nil { log.Errorf("Could not close log rotator: %v", err) } }