Skip to content

Commit 9e4d925

Browse files
committed
cmd/dc: start of derpcat tool
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
1 parent 4453cc5 commit 9e4d925

4 files changed

Lines changed: 197 additions & 15 deletions

File tree

cmd/dc/dc.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"net"
10+
"net/netip"
11+
"os"
12+
"path/filepath"
13+
"time"
14+
15+
"github.com/fxamacker/cbor/v2"
16+
"tailscale.com/ipn/ipnstate"
17+
"tailscale.com/ipn/store"
18+
"tailscale.com/net/netmon"
19+
"tailscale.com/net/netns"
20+
"tailscale.com/net/tsdial"
21+
"tailscale.com/tailcfg"
22+
"tailscale.com/tsd"
23+
"tailscale.com/types/key"
24+
"tailscale.com/types/logger"
25+
"tailscale.com/wgengine"
26+
"tailscale.com/wgengine/netstack"
27+
)
28+
29+
var (
30+
flagServer = flag.Bool("serve", false, "be server")
31+
flagTest = flag.Bool("test", false, "self-test; be a server and connect to ourselves. lazy integration test in package main for now.")
32+
)
33+
34+
func main() {
35+
flag.Parse()
36+
if *flagTest {
37+
doTest()
38+
return
39+
}
40+
if *flagServer {
41+
cb, sys, err := beServer()
42+
if err != nil {
43+
log.Fatal(err)
44+
}
45+
log.Printf(">>> Listening on: %v", cb)
46+
47+
mc := sys.MagicSock.Get()
48+
for {
49+
time.Sleep(5 * time.Second)
50+
var sb ipnstate.StatusBuilder
51+
mc.UpdateStatus(&sb)
52+
log.Printf("status = %v", logger.AsJSON(sb.Status()))
53+
}
54+
}
55+
log.Fatalf("usage: ...")
56+
}
57+
58+
func doTest() {
59+
cb, sys, err := beServer()
60+
if err != nil {
61+
log.Fatal(err)
62+
}
63+
log.Printf(">>> Listening on: %v", cb)
64+
_ = sys
65+
66+
}
67+
68+
// ConnBlob is the base64 encoded cbor of ConnInfo.
69+
type ConnBlob string
70+
71+
type ConnInfo struct {
72+
ServerPub key.NodePublic `cbor:"p"`
73+
Region *tailcfg.DERPRegion `cbor:"r"`
74+
}
75+
76+
func beServer() (ConnBlob, *tsd.System, error) {
77+
var ci ConnInfo
78+
priv := key.NewNode()
79+
ci.ServerPub = priv.Public()
80+
81+
ci.Region = &tailcfg.DERPRegion{
82+
RegionID: 10,
83+
RegionCode: "sea",
84+
Nodes: []*tailcfg.DERPNode{
85+
{
86+
HostName: "derp10b.tailscale.com",
87+
IPv4: "192.73.240.161",
88+
},
89+
{
90+
HostName: "derp10c.tailscale.com",
91+
IPv4: "192.73.240.121",
92+
},
93+
},
94+
}
95+
96+
x, err := cbor.Marshal(&ci)
97+
if err != nil {
98+
return "", nil, fmt.Errorf("cbor.Marshal: %w", err)
99+
}
100+
connBlob := ConnBlob(base64.StdEncoding.EncodeToString(x))
101+
log.Printf(">>> Starting to listen on: %v", connBlob)
102+
103+
var logf logger.Logf = log.Printf
104+
sys := new(tsd.System)
105+
netMon, err := netmon.New(func(format string, args ...any) {
106+
logf(format, args...)
107+
})
108+
if err != nil {
109+
return "", nil, fmt.Errorf("netmon.New: %w", err)
110+
}
111+
sys.Set(netMon)
112+
113+
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
114+
sys.Set(dialer)
115+
116+
store, err := store.New(logf, filepath.Join(os.Getenv("HOME"), ".config", "tsdc.state"))
117+
if err != nil {
118+
return "", nil, fmt.Errorf("store.New: %w", err)
119+
}
120+
sys.Set(store)
121+
122+
if err := createEngine(logf, sys); err != nil {
123+
return "", nil, fmt.Errorf("createEngine: %w", err)
124+
}
125+
ns, err := newNetstack(logf, sys)
126+
if err != nil {
127+
return "", nil, fmt.Errorf("newNetstack: %w", err)
128+
}
129+
ns.ProcessLocalIPs = true
130+
ns.ProcessSubnets = true
131+
132+
e := sys.Engine.Get()
133+
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
134+
_, ok := e.PeerForIP(ip)
135+
return ok
136+
}
137+
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
138+
return ns.DialContextTCP(ctx, dst)
139+
}
140+
141+
if err := ns.Start(nil /* no LocalBackend */); err != nil {
142+
return "", nil, fmt.Errorf("failed to start netstack: %w", err)
143+
}
144+
145+
mc := sys.MagicSock.Get()
146+
log.Printf("disco pub key: %v", mc.DiscoPublicKey())
147+
148+
mc.SetPrivateKey(priv)
149+
e.SetDERPMap(&tailcfg.DERPMap{
150+
Regions: map[int]*tailcfg.DERPRegion{
151+
ci.Region.RegionID: ci.Region,
152+
},
153+
})
154+
mc.SetNetworkUp(true)
155+
156+
return connBlob, sys, nil
157+
}
158+
159+
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
160+
return netstack.Create(logf, sys.Tun.Get(), sys.Engine.Get(), sys.MagicSock.Get(), sys.Dialer.Get(), sys.DNSManager.Get())
161+
}
162+
163+
// createEngine tries to the wgengine.Engine based on the order of tunnels
164+
// specified in the command line flags.
165+
//
166+
// onlyNetstack is true if the user has explicitly requested that we use netstack
167+
// for all networking.
168+
func createEngine(logf logger.Logf, sys *tsd.System) (err error) {
169+
conf := wgengine.Config{
170+
ListenPort: 0,
171+
NetMon: sys.NetMon.Get(),
172+
Dialer: sys.Dialer.Get(),
173+
SetSubsystem: sys.Set,
174+
}
175+
netns.SetEnabled(false)
176+
e, err := wgengine.NewUserspaceEngine(logf, conf)
177+
if err != nil {
178+
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", "userspace-networking", err)
179+
return err
180+
}
181+
e = wgengine.NewWatchdog(e)
182+
sys.Set(e)
183+
sys.NetstackRouter.Set(true)
184+
return nil
185+
}

tailcfg/derpmap.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,16 @@ type DERPRegion struct {
8181
//
8282
// RegionIDs in range 900-999 are reserved for end users to run their
8383
// own DERP nodes.
84-
RegionID int
84+
RegionID int `cbor:"id"`
8585

8686
// RegionCode is a short name for the region. It's usually a popular
8787
// city or airport code in the region: "nyc", "sf", "sin",
8888
// "fra", etc.
89-
RegionCode string
89+
RegionCode string `cbor:",omitempty"`
9090

9191
// RegionName is a long English name for the region: "New York City",
9292
// "San Francisco", "Singapore", "Frankfurt", etc.
93-
RegionName string
93+
RegionName string `cbor:",omitempty"`
9494

9595
// Latitude, Longitude are optional geographical coordinates of the DERP region's city, in degrees.
9696
Latitude float64 `json:",omitempty"`
@@ -134,7 +134,7 @@ type DERPRegion struct {
134134
// for a user/network pick the first one (as they should, when
135135
// things are healthy), the inter-cluster routing is minimal
136136
// to zero.
137-
Nodes []*DERPNode
137+
Nodes []*DERPNode `cbor:"n"`
138138
}
139139

140140
// DERPNode describes a DERP packet relay node running within a DERPRegion.
@@ -143,17 +143,17 @@ type DERPNode struct {
143143
// It is not a host name.
144144
// It's typically of the form "1b", "2a", "3b", etc. (region
145145
// ID + suffix within that region)
146-
Name string
146+
Name string `json:",omitempty"`
147147

148148
// RegionID is the RegionID of the DERPRegion that this node
149149
// is running in.
150-
RegionID int
150+
RegionID int `json:",omitempty"`
151151

152152
// HostName is the DERP node's hostname.
153153
//
154154
// It is required but need not be unique; multiple nodes may
155155
// have the same HostName but vary in configuration otherwise.
156-
HostName string
156+
HostName string `cbor:"hn,omitempty"`
157157

158158
// CertName optionally specifies the expected TLS cert common
159159
// name. If empty, HostName is used. If CertName is non-empty,
@@ -172,14 +172,14 @@ type DERPNode struct {
172172
// If the string is not an IPv4 address, IPv4 is not used; the
173173
// conventional string to disable IPv4 (and not use DNS) is
174174
// "none".
175-
IPv4 string `json:",omitempty"`
175+
IPv4 string `json:",omitempty" cbor:"4,omitempty"`
176176

177177
// IPv6 optionally forces an IPv6 address to use, instead of using DNS.
178178
// If empty, AAAA record(s) from DNS lookups of HostName are used.
179179
// If the string is not an IPv6 address, IPv6 is not used; the
180180
// conventional string to disable IPv6 (and not use DNS) is
181181
// "none".
182-
IPv6 string `json:",omitempty"`
182+
IPv6 string `json:",omitempty" cbor:"6,omitempty"`
183183

184184
// Port optionally specifies a STUN port to use.
185185
// Zero means 3478.

wgengine/magicsock/derp.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bufio"
88
"context"
99
"fmt"
10+
"log"
1011
"maps"
1112
"net"
1213
"net/netip"
@@ -252,6 +253,8 @@ func (c *Conn) setNearestDERP(derpNum int) (wantDERP bool) {
252253
c.setHomeDERPGaugeLocked(derpNum)
253254
c.health.SetMagicSockDERPHome(derpNum, c.homeless)
254255

256+
log.Printf("XXX derp: setNearestDERP: derpNum=%v, key=%v", derpNum, !c.privateKey.IsZero())
257+
255258
if c.privateKey.IsZero() {
256259
// No private key yet, so DERP connections won't come up anyway.
257260
// Return early rather than ultimately log a couple lines of noise.

wgengine/netstack/netstack.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -604,13 +604,7 @@ type LocalBackend = any
604604
// Start sets up all the handlers so netstack can start working. Implements
605605
// wgengine.FakeImpl.
606606
func (ns *Impl) Start(b LocalBackend) error {
607-
if b == nil {
608-
panic("nil LocalBackend interface")
609-
}
610607
lb := b.(*ipnlocal.LocalBackend)
611-
if lb == nil {
612-
panic("nil LocalBackend")
613-
}
614608
ns.lb = lb
615609
tcpFwd := tcp.NewForwarder(ns.ipstack, tcpRXBufDefSize, maxInFlightConnectionAttempts(), ns.acceptTCP)
616610
udpFwd := udp.NewForwarder(ns.ipstack, ns.acceptUDPNoICMP)

0 commit comments

Comments
 (0)