cmd/lopower: add QR code handler

Change-Id: I0c379cfeff9855b745ba705beb574dab6d26b305
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2024-11-02 14:45:24 -07:00 committed by Anton Tolchanov
parent 5ee9896a09
commit 922d65ed11
2 changed files with 101 additions and 13 deletions

View File

@ -4,21 +4,28 @@
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io" "io"
"log" "log"
"net"
"net/http"
"net/netip" "net/netip"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"slices" "slices"
"strings"
"sync" "sync"
"time" "time"
qrcode "github.com/skip2/go-qrcode"
"github.com/tailscale/wireguard-go/conn" "github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun" "github.com/tailscale/wireguard-go/tun"
@ -35,6 +42,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp" "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/waiter" "gvisor.dev/gvisor/pkg/waiter"
"tailscale.com/net/packet" "tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
"tailscale.com/tsnet" "tailscale.com/tsnet"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
@ -44,12 +52,14 @@ import (
var ( var (
wgListenPort = flag.Int("wg-port", 51820, "port number to listen on for WireGuard from the client") wgListenPort = flag.Int("wg-port", 51820, "port number to listen on for WireGuard from the client")
qrListenAddr = flag.String("qr-listen", "127.0.0.1:8014", "HTTP address to serve a QR code for client's WireGuard configuration")
confDir = flag.String("dir", filepath.Join(os.Getenv("HOME"), ".config/lopower"), "directory to store configuration in") confDir = flag.String("dir", filepath.Join(os.Getenv("HOME"), ".config/lopower"), "directory to store configuration in")
wgPubHost = flag.String("wg-host", "0.0.0.1", "public IP address of lopower's WireGuard server")
qrListenAddr = flag.String("qr-listen", "127.0.0.1:8014", "HTTP address to serve a QR code for client's WireGuard configuration, or empty for none")
printConfig = flag.Bool("print-config", true, "print the client's WireGuard configuration to stdout on startup")
) )
type config struct { type config struct {
PrivKey key.NodePrivate PrivKey key.NodePrivate // the proxy server's key
Peers []Peer Peers []Peer
// V4 and V6 are the local IPs. // V4 and V6 are the local IPs.
@ -62,9 +72,9 @@ type config struct {
} }
type Peer struct { type Peer struct {
PubKey key.NodePublic PrivKey key.NodePrivate // e.g. proxy client's
V4 netip.Addr V4 netip.Addr
V6 netip.Addr V6 netip.Addr
} }
func (lp *lpServer) storeConfigLocked() { func (lp *lpServer) storeConfigLocked() {
@ -85,21 +95,23 @@ func (lp *lpServer) storeConfigLocked() {
func (lp *lpServer) loadConfig() { func (lp *lpServer) loadConfig() {
path := filepath.Join(lp.dir, "config.json") path := filepath.Join(lp.dir, "config.json")
f, err := os.OpenFile(path, os.O_RDONLY, 0) f, err := os.Open(path)
if err == nil { if err == nil {
defer f.Close() defer f.Close()
var cfg *config var cfg *config
must.Do(json.NewDecoder(f).Decode(&cfg)) must.Do(json.NewDecoder(f).Decode(&cfg))
lp.mu.Lock() if len(cfg.Peers) > 0 { // as early version didn't set this
defer lp.mu.Unlock() lp.mu.Lock()
lp.c = cfg defer lp.mu.Unlock()
lp.c = cfg
}
return return
} }
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
log.Fatalf("os.OpenFile(%q): %v", path, err) log.Fatalf("os.OpenFile(%q): %v", path, err)
} }
const defaultV4CIDR = "10.90.0.0/24" const defaultV4CIDR = "10.90.0.0/24"
const defaultV6CIDR = "fd7a:115c:a1e0:1900::/64" const defaultV6CIDR = "fd7a:115c:a1e0:9909::/64" // 9909 = above QWERTY "LOPO"(wer)
c := &config{ c := &config{
PrivKey: key.NewNode(), PrivKey: key.NewNode(),
V4CIDR: netip.MustParsePrefix(defaultV4CIDR), V4CIDR: netip.MustParsePrefix(defaultV4CIDR),
@ -107,6 +119,12 @@ func (lp *lpServer) loadConfig() {
} }
c.V4 = c.V4CIDR.Addr().Next() c.V4 = c.V4CIDR.Addr().Next()
c.V6 = c.V6CIDR.Addr().Next() c.V6 = c.V6CIDR.Addr().Next()
c.Peers = append(c.Peers, Peer{
PrivKey: key.NewNode(),
V4: c.V4.Next(),
V6: c.V6.Next(),
})
lp.mu.Lock() lp.mu.Lock()
defer lp.mu.Unlock() defer lp.mu.Unlock()
lp.c = c lp.c = c
@ -127,7 +145,7 @@ func (lp *lpServer) reconfig() {
} }
for _, p := range lp.c.Peers { for _, p := range lp.c.Peers {
wc.Peers = append(wc.Peers, wgcfg.Peer{ wc.Peers = append(wc.Peers, wgcfg.Peer{
PublicKey: p.PubKey, PublicKey: p.PrivKey.Public(),
AllowedIPs: []netip.Prefix{ AllowedIPs: []netip.Prefix{
netip.PrefixFrom(p.V4, 32), netip.PrefixFrom(p.V4, 32),
netip.PrefixFrom(p.V6, 128), netip.PrefixFrom(p.V6, 128),
@ -154,11 +172,17 @@ func newLP(ctx context.Context) *lpServer {
closeCh: make(chan struct{}), closeCh: make(chan struct{}),
evChan: make(chan tun.Event), evChan: make(chan tun.Event),
} }
wgdev := wgcfg.NewDevice(nst, conn.NewDefaultBind(), deviceLogger) wgdev := wgcfg.NewDevice(nst, conn.NewDefaultBind(), deviceLogger)
defer wgdev.Close() defer wgdev.Close()
lp.d = wgdev lp.d = wgdev
must.Do(wgdev.Up()) must.Do(wgdev.Up())
lp.reconfig() lp.reconfig()
if *printConfig {
log.Printf("Device Wireguard config is:\n%s", lp.wgConfigForQR())
}
lp.startTSNet(ctx) lp.startTSNet(ctx)
return lp return lp
} }
@ -320,6 +344,63 @@ func (lp *lpServer) acceptTCP(r *tcp.ForwarderRequest) {
<-errc <-errc
} }
func (lp *lpServer) wgConfigForQR() string {
var b strings.Builder
privHex, _ := lp.c.Peers[0].PrivKey.MarshalText()
privHex = bytes.TrimPrefix(privHex, []byte("privkey:"))
priv := make([]byte, 32)
got, err := hex.Decode(priv, privHex)
if err != nil || got != 32 {
log.Printf("marshal text was: %q", privHex)
log.Fatalf("bad private key: %v, % bytes", err, got)
}
privb64 := base64.StdEncoding.EncodeToString(priv)
fmt.Fprintf(&b, "[Interface]\nPrivateKey = %s\n", privb64)
fmt.Fprintf(&b, "Address = %v\n", lp.c.V6)
pubBin, _ := lp.c.PrivKey.Public().MarshalBinary()
if len(pubBin) != 34 {
log.Fatalf("bad pubkey length: %d", len(pubBin))
}
pubBin = pubBin[2:] // trim off "np"
pubb64 := base64.StdEncoding.EncodeToString(pubBin)
fmt.Fprintf(&b, "[Peer]\nPublicKey = %v\n", pubb64)
fmt.Fprintf(&b, "AllowedIPs = %v\n", tsaddr.TailscaleULARange())
fmt.Fprintf(&b, "Endpoint = %v\n", net.JoinHostPort(*wgPubHost, fmt.Sprint(*wgListenPort)))
return b.String()
}
func (lp *lpServer) serveQR() {
ln, err := net.Listen("tcp", *qrListenAddr)
if err != nil {
log.Fatalf("qr: %v", err)
}
log.Printf("# Serving QR code at http://%s/", ln.Addr())
hs := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "image/png")
conf := lp.wgConfigForQR()
v, err := qrcode.Encode(conf, qrcode.Medium, 512)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(v)
}),
}
if err := hs.Serve(ln); err != nil {
log.Fatalf("qr: %v", err)
}
}
type nsTUN struct { type nsTUN struct {
lp *lpServer lp *lpServer
closeCh chan struct{} closeCh chan struct{}
@ -398,11 +479,16 @@ func (lp *lpServer) startTSNet(ctx context.Context) {
func main() { func main() {
flag.Parse() flag.Parse()
log.Printf("lopower starting")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
lp := newLP(ctx) lp := newLP(ctx)
_ = lp
if *qrListenAddr != "" {
go lp.serveQR()
}
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, unix.SIGTERM, os.Interrupt) signal.Notify(sigCh, unix.SIGTERM, os.Interrupt)

View File

@ -90,7 +90,9 @@ func (cfg *Config) ToUAPI(logf logger.Logf, w io.Writer, prev *Config) error {
// See corp issue 3016. // See corp issue 3016.
logf("[unexpected] endpoint changed from %s to %s", oldPeer.WGEndpoint, p.PublicKey) logf("[unexpected] endpoint changed from %s to %s", oldPeer.WGEndpoint, p.PublicKey)
} }
set("endpoint", p.PublicKey.UntypedHexString()) if cfg.NodeID != "" {
set("endpoint", p.PublicKey.UntypedHexString())
}
} }
// TODO: replace_allowed_ips is expensive. // TODO: replace_allowed_ips is expensive.