mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
393 lines
12 KiB
Go
393 lines
12 KiB
Go
package tor
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
var (
|
|
// ErrEncryptedTorPrivateKey is thrown when a tor private key is
|
|
// encrypted, but the user requested an unencrypted key.
|
|
ErrEncryptedTorPrivateKey = errors.New("it appears the Tor private key " +
|
|
"is encrypted but you didn't pass the --tor.encryptkey flag. " +
|
|
"Please restart lnd with the --tor.encryptkey flag or delete " +
|
|
"the Tor key file for regeneration")
|
|
|
|
// ErrNoPrivateKey is an error returned by the OnionStore.PrivateKey
|
|
// method when a private key hasn't yet been stored.
|
|
ErrNoPrivateKey = errors.New("private key not found")
|
|
)
|
|
|
|
// OnionType denotes the type of the onion service.
|
|
type OnionType int
|
|
|
|
const (
|
|
// V2 denotes that the onion service is V2.
|
|
V2 OnionType = iota
|
|
|
|
// V3 denotes that the onion service is V3.
|
|
V3
|
|
)
|
|
|
|
const (
|
|
// V2KeyParam is a parameter that Tor accepts for a new V2 service.
|
|
V2KeyParam = "RSA1024"
|
|
|
|
// V3KeyParam is a parameter that Tor accepts for a new V3 service.
|
|
V3KeyParam = "ED25519-V3"
|
|
)
|
|
|
|
// OnionStore is a store containing information about a particular onion
|
|
// service.
|
|
type OnionStore interface {
|
|
// StorePrivateKey stores the private key according to the
|
|
// implementation of the OnionStore interface.
|
|
StorePrivateKey([]byte) error
|
|
|
|
// PrivateKey retrieves a stored private key. If it is not found, then
|
|
// ErrNoPrivateKey should be returned.
|
|
PrivateKey() ([]byte, error)
|
|
|
|
// DeletePrivateKey securely removes the private key from the store.
|
|
DeletePrivateKey() error
|
|
}
|
|
|
|
// EncrypterDecrypter is used for encrypting and decrypting the onion service
|
|
// private key.
|
|
type EncrypterDecrypter interface {
|
|
EncryptPayloadToWriter([]byte, io.Writer) error
|
|
DecryptPayloadFromReader(io.Reader) ([]byte, error)
|
|
}
|
|
|
|
// OnionFile is a file-based implementation of the OnionStore interface that
|
|
// stores an onion service's private key.
|
|
type OnionFile struct {
|
|
privateKeyPath string
|
|
privateKeyPerm os.FileMode
|
|
encryptKey bool
|
|
encrypter EncrypterDecrypter
|
|
}
|
|
|
|
// A compile-time constraint to ensure OnionFile satisfies the OnionStore
|
|
// interface.
|
|
var _ OnionStore = (*OnionFile)(nil)
|
|
|
|
// NewOnionFile creates a file-based implementation of the OnionStore interface
|
|
// to store an onion service's private key.
|
|
func NewOnionFile(privateKeyPath string, privateKeyPerm os.FileMode,
|
|
encryptKey bool, encrypter EncrypterDecrypter) *OnionFile {
|
|
|
|
return &OnionFile{
|
|
privateKeyPath: privateKeyPath,
|
|
privateKeyPerm: privateKeyPerm,
|
|
encryptKey: encryptKey,
|
|
encrypter: encrypter,
|
|
}
|
|
}
|
|
|
|
// StorePrivateKey stores the private key at its expected path. It also
|
|
// encrypts the key before storing it if requested.
|
|
func (f *OnionFile) StorePrivateKey(privateKey []byte) error {
|
|
privateKeyContent := privateKey
|
|
|
|
if f.encryptKey {
|
|
var b bytes.Buffer
|
|
err := f.encrypter.EncryptPayloadToWriter(
|
|
privateKey, &b,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
privateKeyContent = b.Bytes()
|
|
}
|
|
|
|
err := os.WriteFile(
|
|
f.privateKeyPath, privateKeyContent, f.privateKeyPerm,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write private key "+
|
|
"to file: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PrivateKey retrieves the private key from its expected path. If the file does
|
|
// not exist, then ErrNoPrivateKey is returned.
|
|
func (f *OnionFile) PrivateKey() ([]byte, error) {
|
|
_, err := os.Stat(f.privateKeyPath)
|
|
if err != nil && errors.Is(err, os.ErrNotExist) {
|
|
return nil, ErrNoPrivateKey
|
|
}
|
|
|
|
// Try to read the Tor private key to pass into the AddOnion call.
|
|
privateKeyContent, err := os.ReadFile(f.privateKeyPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the privateKey starts with either v2 or v3 key params then
|
|
// it's likely not encrypted and we can return the data as is.
|
|
if bytes.HasPrefix(privateKeyContent, []byte(V2KeyParam)) ||
|
|
bytes.HasPrefix(privateKeyContent, []byte(V3KeyParam)) {
|
|
|
|
return privateKeyContent, nil
|
|
}
|
|
|
|
// If the privateKeyContent is encrypted but --tor.encryptkey
|
|
// wasn't set we return an error.
|
|
if !f.encryptKey {
|
|
return nil, ErrEncryptedTorPrivateKey
|
|
}
|
|
|
|
// Attempt to decrypt the key.
|
|
reader := bytes.NewReader(privateKeyContent)
|
|
privateKeyContent, err = f.encrypter.DecryptPayloadFromReader(
|
|
reader,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return privateKeyContent, nil
|
|
}
|
|
|
|
// DeletePrivateKey removes the file containing the private key.
|
|
func (f *OnionFile) DeletePrivateKey() error {
|
|
return os.Remove(f.privateKeyPath)
|
|
}
|
|
|
|
// AddOnionConfig houses all of the required parameters in order to
|
|
// successfully create a new onion service or restore an existing one.
|
|
type AddOnionConfig struct {
|
|
// Type denotes the type of the onion service that should be created.
|
|
Type OnionType
|
|
|
|
// VirtualPort is the externally reachable port of the onion address.
|
|
VirtualPort int
|
|
|
|
// TargetPorts is the set of ports that the service will be listening
|
|
// on locally. The Tor server will use choose a random port from this
|
|
// set to forward the traffic from the virtual port.
|
|
//
|
|
// NOTE: If nil/empty, the virtual port will be used as the only target
|
|
// port.
|
|
TargetPorts []int
|
|
|
|
// Store is responsible for storing all onion service related
|
|
// information.
|
|
//
|
|
// NOTE: If not specified, then nothing will be stored, making onion
|
|
// services unrecoverable after shutdown.
|
|
Store OnionStore
|
|
}
|
|
|
|
// prepareKeyparam takes a config and prepares the key param to be used inside
|
|
// ADD_ONION.
|
|
func (c *Controller) prepareKeyparam(cfg AddOnionConfig) (string, error) {
|
|
// We'll start off by checking if the store contains an existing
|
|
// private key. If it does not, then we should request the server to
|
|
// create a new onion service and return its private key. Otherwise,
|
|
// we'll request the server to recreate the onion server from our
|
|
// private key.
|
|
var keyParam string
|
|
switch cfg.Type {
|
|
// TODO(yy): drop support for v2.
|
|
case V2:
|
|
keyParam = "NEW:" + V2KeyParam
|
|
case V3:
|
|
keyParam = "NEW:" + V3KeyParam
|
|
}
|
|
|
|
if cfg.Store != nil {
|
|
privateKey, err := cfg.Store.PrivateKey()
|
|
switch err {
|
|
// Proceed to request a new onion service.
|
|
case ErrNoPrivateKey:
|
|
|
|
// Recover the onion service with the private key found.
|
|
case nil:
|
|
keyParam = string(privateKey)
|
|
|
|
default:
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return keyParam, nil
|
|
}
|
|
|
|
// prepareAddOnion constructs a cmd command string based on the specified
|
|
// config.
|
|
func (c *Controller) prepareAddOnion(cfg AddOnionConfig) (string, string,
|
|
error) {
|
|
|
|
// Create the keyParam.
|
|
keyParam, err := c.prepareKeyparam(cfg)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Now, we'll create a mapping from the virtual port to each target
|
|
// port. If no target ports were specified, we'll use the virtual port
|
|
// to provide a one-to-one mapping.
|
|
var portParam string
|
|
|
|
// Helper function which appends the correct Port param depending on
|
|
// whether the user chose to use a custom target IP address or not.
|
|
pushPortParam := func(targetPort int) {
|
|
if c.targetIPAddress == "" {
|
|
portParam += fmt.Sprintf("Port=%d,%d ", cfg.VirtualPort,
|
|
targetPort)
|
|
} else {
|
|
portParam += fmt.Sprintf("Port=%d,%s:%d ",
|
|
cfg.VirtualPort, c.targetIPAddress, targetPort)
|
|
}
|
|
}
|
|
|
|
if len(cfg.TargetPorts) == 0 {
|
|
pushPortParam(cfg.VirtualPort)
|
|
} else {
|
|
for _, targetPort := range cfg.TargetPorts {
|
|
pushPortParam(targetPort)
|
|
}
|
|
}
|
|
|
|
// Send the command to create the onion service to the Tor server and
|
|
// await its response.
|
|
cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam)
|
|
|
|
return cmd, keyParam, nil
|
|
}
|
|
|
|
// AddOnion creates an ephemeral onion service and returns its onion address.
|
|
// Once created, the new onion service will remain active until either,
|
|
// - the onion service is removed via `DEL_ONION`.
|
|
// - the Tor daemon terminates.
|
|
// - the controller connection that originated the `ADD_ONION` is closed.
|
|
//
|
|
// Each connection can only see its own ephemeral services. If a service needs
|
|
// to survive beyond current controller connection, use the "Detach" flag when
|
|
// creating new service via `ADD_ONION`.
|
|
func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
|
|
// Before sending the request to create an onion service to the Tor
|
|
// server, we'll make sure that it supports V3 onion services if that
|
|
// was the type requested.
|
|
// TODO(yy): drop support for v2.
|
|
if cfg.Type == V3 {
|
|
if err := supportsV3(c.version); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Construct the cmd command.
|
|
cmd, keyParam, err := c.prepareAddOnion(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Send the command to create the onion service to the Tor server and
|
|
// await its response.
|
|
_, reply, err := c.sendCommand(cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If successful, the reply from the server should be of the following
|
|
// format, depending on whether a private key has been requested:
|
|
//
|
|
// C: ADD_ONION RSA1024:[Blob Redacted] Port=80,8080
|
|
// S: 250-ServiceID=testonion1234567
|
|
// S: 250 OK
|
|
//
|
|
// C: ADD_ONION NEW:RSA1024 Port=80,8080
|
|
// S: 250-ServiceID=testonion1234567
|
|
// S: 250-PrivateKey=RSA1024:[Blob Redacted]
|
|
// S: 250 OK
|
|
//
|
|
// We're interested in retrieving the service ID, which is the public
|
|
// name of the service, and the private key if requested.
|
|
replyParams := parseTorReply(reply)
|
|
serviceID, ok := replyParams["ServiceID"]
|
|
if !ok {
|
|
return nil, errors.New("service id not found in reply")
|
|
}
|
|
|
|
// If a new onion service was created, use the new private key for
|
|
// storage.
|
|
newPrivateKey, ok := replyParams["PrivateKey"]
|
|
if ok {
|
|
keyParam = newPrivateKey
|
|
}
|
|
|
|
// If an onion store was provided and a key return wasn't requested,
|
|
// we'll store its private key to disk in the event that it needs to
|
|
// be recreated later on. We write the private key to disk every time
|
|
// in case the user toggles the --tor.encryptkey flag.
|
|
if cfg.Store != nil {
|
|
err := cfg.Store.StorePrivateKey([]byte(keyParam))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to write private key "+
|
|
"to file: %v", err)
|
|
}
|
|
}
|
|
|
|
c.activeServiceID = serviceID
|
|
log.Debugf("serviceID:%s added to tor controller", serviceID)
|
|
|
|
// Finally, we'll return the onion address composed of the service ID,
|
|
// along with the onion suffix, and the port this onion service can be
|
|
// reached at externally.
|
|
return &OnionAddr{
|
|
OnionService: serviceID + ".onion",
|
|
Port: cfg.VirtualPort,
|
|
PrivateKey: keyParam,
|
|
}, nil
|
|
}
|
|
|
|
// DelOnion tells the Tor daemon to remove an onion service, which satisfies
|
|
// either,
|
|
// - the onion service was created on the same control connection as the
|
|
// "DEL_ONION" command.
|
|
// - the onion service was created using the "Detach" flag.
|
|
func (c *Controller) DelOnion(serviceID string) error {
|
|
log.Debugf("removing serviceID:%s from tor controller", serviceID)
|
|
|
|
cmd := fmt.Sprintf("DEL_ONION %s", serviceID)
|
|
|
|
// Send the command to create the onion service to the Tor server and
|
|
// await its response.
|
|
code, _, err := c.sendCommand(cmd)
|
|
|
|
// Tor replies with "250 OK" on success, or a 512 if there are an
|
|
// invalid number of arguments, or a 552 if it doesn't recognize the
|
|
// ServiceID.
|
|
switch code {
|
|
// Replied 250 OK.
|
|
case success:
|
|
return nil
|
|
|
|
// Replied 512 for invalid arguments. This is most likely that the
|
|
// serviceID is not set(empty string).
|
|
case invalidNumOfArguments:
|
|
return fmt.Errorf("invalid arguments: %w", err)
|
|
|
|
// Replied 552, which means either,
|
|
// - the serviceID is invalid.
|
|
// - the onion service is not owned by the current control connection
|
|
// and,
|
|
// - the onion service is not a detached service.
|
|
// In either case, we will ignore the error and log a warning as there
|
|
// not much we can do from the controller side.
|
|
case serviceIDNotRecognized:
|
|
log.Warnf("removing serviceID:%v not found", serviceID)
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("undefined response code: %v, err: %w",
|
|
code, err)
|
|
}
|
|
}
|