Skip to content

Commit 4cb518c

Browse files
authored
Merge pull request lightningnetwork#4056 from wpaulino/tor-onion-store
tor+server: add OnionStore to AddOnionConfig with file-based implementation
2 parents d570cb0 + 2dfd6a7 commit 4cb518c

File tree

4 files changed

+271
-145
lines changed

4 files changed

+271
-145
lines changed

server.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -1977,9 +1977,9 @@ func (s *server) initTorController() error {
19771977
// create our onion service. The service's private key will be saved to
19781978
// disk in order to regain access to this service when restarting `lnd`.
19791979
onionCfg := tor.AddOnionConfig{
1980-
VirtualPort: defaultPeerPort,
1981-
TargetPorts: listenPorts,
1982-
PrivateKeyPath: cfg.Tor.PrivateKeyPath,
1980+
VirtualPort: defaultPeerPort,
1981+
TargetPorts: listenPorts,
1982+
Store: tor.NewOnionFile(cfg.Tor.PrivateKeyPath, 0600),
19831983
}
19841984

19851985
switch {

tor/add_onion.go

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package tor
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
)
9+
10+
var (
11+
// ErrNoPrivateKey is an error returned by the OnionStore.PrivateKey
12+
// method when a private key hasn't yet been stored.
13+
ErrNoPrivateKey = errors.New("private key not found")
14+
)
15+
16+
// OnionType denotes the type of the onion service.
17+
type OnionType int
18+
19+
const (
20+
// V2 denotes that the onion service is V2.
21+
V2 OnionType = iota
22+
23+
// V3 denotes that the onion service is V3.
24+
V3
25+
)
26+
27+
// OnionStore is a store containing information about a particular onion
28+
// service.
29+
type OnionStore interface {
30+
// StorePrivateKey stores the private key according to the
31+
// implementation of the OnionStore interface.
32+
StorePrivateKey(OnionType, []byte) error
33+
34+
// PrivateKey retrieves a stored private key. If it is not found, then
35+
// ErrNoPrivateKey should be returned.
36+
PrivateKey(OnionType) ([]byte, error)
37+
38+
// DeletePrivateKey securely removes the private key from the store.
39+
DeletePrivateKey(OnionType) error
40+
}
41+
42+
// OnionFile is a file-based implementation of the OnionStore interface that
43+
// stores an onion service's private key.
44+
type OnionFile struct {
45+
privateKeyPath string
46+
privateKeyPerm os.FileMode
47+
}
48+
49+
// A compile-time constraint to ensure OnionFile satisfies the OnionStore
50+
// interface.
51+
var _ OnionStore = (*OnionFile)(nil)
52+
53+
// NewOnionFile creates a file-based implementation of the OnionStore interface
54+
// to store an onion service's private key.
55+
func NewOnionFile(privateKeyPath string, privateKeyPerm os.FileMode) *OnionFile {
56+
return &OnionFile{
57+
privateKeyPath: privateKeyPath,
58+
privateKeyPerm: privateKeyPerm,
59+
}
60+
}
61+
62+
// StorePrivateKey stores the private key at its expected path.
63+
func (f *OnionFile) StorePrivateKey(_ OnionType, privateKey []byte) error {
64+
return ioutil.WriteFile(f.privateKeyPath, privateKey, f.privateKeyPerm)
65+
}
66+
67+
// PrivateKey retrieves the private key from its expected path. If the file does
68+
// not exist, then ErrNoPrivateKey is returned.
69+
func (f *OnionFile) PrivateKey(_ OnionType) ([]byte, error) {
70+
if _, err := os.Stat(f.privateKeyPath); os.IsNotExist(err) {
71+
return nil, ErrNoPrivateKey
72+
}
73+
return ioutil.ReadFile(f.privateKeyPath)
74+
}
75+
76+
// DeletePrivateKey removes the file containing the private key.
77+
func (f *OnionFile) DeletePrivateKey(_ OnionType) error {
78+
return os.Remove(f.privateKeyPath)
79+
}
80+
81+
// AddOnionConfig houses all of the required parameters in order to successfully
82+
// create a new onion service or restore an existing one.
83+
type AddOnionConfig struct {
84+
// Type denotes the type of the onion service that should be created.
85+
Type OnionType
86+
87+
// VirtualPort is the externally reachable port of the onion address.
88+
VirtualPort int
89+
90+
// TargetPorts is the set of ports that the service will be listening on
91+
// locally. The Tor server will use choose a random port from this set
92+
// to forward the traffic from the virtual port.
93+
//
94+
// NOTE: If nil/empty, the virtual port will be used as the only target
95+
// port.
96+
TargetPorts []int
97+
98+
// Store is responsible for storing all onion service related
99+
// information.
100+
//
101+
// NOTE: If not specified, then nothing will be stored, making onion
102+
// services unrecoverable after shutdown.
103+
Store OnionStore
104+
}
105+
106+
// AddOnion creates an onion service and returns its onion address. Once
107+
// created, the new onion service will remain active until the connection
108+
// between the controller and the Tor server is closed.
109+
func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
110+
// Before sending the request to create an onion service to the Tor
111+
// server, we'll make sure that it supports V3 onion services if that
112+
// was the type requested.
113+
if cfg.Type == V3 {
114+
if err := supportsV3(c.version); err != nil {
115+
return nil, err
116+
}
117+
}
118+
119+
// We'll start off by checking if the store contains an existing private
120+
// key. If it does not, then we should request the server to create a
121+
// new onion service and return its private key. Otherwise, we'll
122+
// request the server to recreate the onion server from our private key.
123+
var keyParam string
124+
switch cfg.Type {
125+
case V2:
126+
keyParam = "NEW:RSA1024"
127+
case V3:
128+
keyParam = "NEW:ED25519-V3"
129+
}
130+
131+
if cfg.Store != nil {
132+
privateKey, err := cfg.Store.PrivateKey(cfg.Type)
133+
switch err {
134+
// Proceed to request a new onion service.
135+
case ErrNoPrivateKey:
136+
137+
// Recover the onion service with the private key found.
138+
case nil:
139+
keyParam = string(privateKey)
140+
141+
default:
142+
return nil, err
143+
}
144+
}
145+
146+
// Now, we'll create a mapping from the virtual port to each target
147+
// port. If no target ports were specified, we'll use the virtual port
148+
// to provide a one-to-one mapping.
149+
var portParam string
150+
151+
// Helper function which appends the correct Port param depending on
152+
// whether the user chose to use a custom target IP address or not.
153+
pushPortParam := func(targetPort int) {
154+
if c.targetIPAddress == "" {
155+
portParam += fmt.Sprintf("Port=%d,%d ", cfg.VirtualPort,
156+
targetPort)
157+
} else {
158+
portParam += fmt.Sprintf("Port=%d,%s:%d ", cfg.VirtualPort,
159+
c.targetIPAddress, targetPort)
160+
}
161+
}
162+
163+
if len(cfg.TargetPorts) == 0 {
164+
pushPortParam(cfg.VirtualPort)
165+
} else {
166+
for _, targetPort := range cfg.TargetPorts {
167+
pushPortParam(targetPort)
168+
}
169+
}
170+
171+
// Send the command to create the onion service to the Tor server and
172+
// await its response.
173+
cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam)
174+
_, reply, err := c.sendCommand(cmd)
175+
if err != nil {
176+
return nil, err
177+
}
178+
179+
// If successful, the reply from the server should be of the following
180+
// format, depending on whether a private key has been requested:
181+
//
182+
// C: ADD_ONION RSA1024:[Blob Redacted] Port=80,8080
183+
// S: 250-ServiceID=testonion1234567
184+
// S: 250 OK
185+
//
186+
// C: ADD_ONION NEW:RSA1024 Port=80,8080
187+
// S: 250-ServiceID=testonion1234567
188+
// S: 250-PrivateKey=RSA1024:[Blob Redacted]
189+
// S: 250 OK
190+
//
191+
// We're interested in retrieving the service ID, which is the public
192+
// name of the service, and the private key if requested.
193+
replyParams := parseTorReply(reply)
194+
serviceID, ok := replyParams["ServiceID"]
195+
if !ok {
196+
return nil, errors.New("service id not found in reply")
197+
}
198+
199+
// If a new onion service was created and an onion store was provided,
200+
// we'll store its private key to disk in the event that it needs to be
201+
// recreated later on.
202+
if privateKey, ok := replyParams["PrivateKey"]; cfg.Store != nil && ok {
203+
err := cfg.Store.StorePrivateKey(cfg.Type, []byte(privateKey))
204+
if err != nil {
205+
return nil, fmt.Errorf("unable to write private key "+
206+
"to file: %v", err)
207+
}
208+
}
209+
210+
// Finally, we'll return the onion address composed of the service ID,
211+
// along with the onion suffix, and the port this onion service can be
212+
// reached at externally.
213+
return &OnionAddr{
214+
OnionService: serviceID + ".onion",
215+
Port: cfg.VirtualPort,
216+
}, nil
217+
}

tor/add_onion_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package tor
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
// TestOnionFile tests that the OnionFile implementation of the OnionStore
11+
// interface behaves as expected.
12+
func TestOnionFile(t *testing.T) {
13+
t.Parallel()
14+
15+
tempDir, err := ioutil.TempDir("", "onion_store")
16+
if err != nil {
17+
t.Fatalf("unable to create temp dir: %v", err)
18+
}
19+
20+
privateKey := []byte("hide_me_plz")
21+
privateKeyPath := filepath.Join(tempDir, "secret")
22+
23+
// Create a new file-based onion store. A private key should not exist
24+
// yet.
25+
onionFile := NewOnionFile(privateKeyPath, 0600)
26+
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey {
27+
t.Fatalf("expected ErrNoPrivateKey, got \"%v\"", err)
28+
}
29+
30+
// Store the private key and ensure what's stored matches.
31+
if err := onionFile.StorePrivateKey(V2, privateKey); err != nil {
32+
t.Fatalf("unable to store private key: %v", err)
33+
}
34+
storePrivateKey, err := onionFile.PrivateKey(V2)
35+
if err != nil {
36+
t.Fatalf("unable to retrieve private key: %v", err)
37+
}
38+
if !bytes.Equal(storePrivateKey, privateKey) {
39+
t.Fatalf("expected private key \"%v\", got \"%v\"",
40+
string(privateKey), string(storePrivateKey))
41+
}
42+
43+
// Finally, delete the private key. We should no longer be able to
44+
// retrieve it.
45+
if err := onionFile.DeletePrivateKey(V2); err != nil {
46+
t.Fatalf("unable to delete private key: %v", err)
47+
}
48+
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey {
49+
t.Fatal("found deleted private key")
50+
}
51+
}

0 commit comments

Comments
 (0)