You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/natconnector/natconnector.go

209 lines
5.9 KiB
Go

3 months ago
package natconnector
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/netip"
"github.com/inetaf/tcpproxy"
"golang.org/x/net/dns/dnsmessage"
ippool "tailscale.com/natcippool"
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
)
type NatConnector struct {
logf logger.Logf
ConsensusClient *ippool.ConsensusClient
whoIs func(string, netip.AddrPort) (tailcfg.NodeView, tailcfg.UserProfile, bool)
}
func (n *NatConnector) HandleDNSQuery(ctx context.Context, query []byte, remoteAddr netip.AddrPort) ([]byte, error, bool) {
// TODO even though because of the way the netmap instructions for dns work we can expect only to
// get dns requests for domains that are configured in the acls, we should probably check the domain
// here anyway
// edit: actually I wonder if there are cases we might end up getting a req through here that isn't for us, just
// because a node is offering a nat connector does that mean all doh queries are for the nat connector?
var msg dnsmessage.Message
err := msg.Unpack(query)
if err != nil {
log.Printf("HandleDNSQuery: dnsmessage unpack failed: %v\n ", err)
return nil, err, true
}
// who's asking?
nodeView, _, ok := n.whoIs("", remoteAddr)
if !ok {
log.Printf("HandleDNSQuery: WhoIs invalid for: %v\n", remoteAddr)
return nil, errors.New("invalid remoteAddr"), true // TODO
}
domain := msg.Questions[0].Name.String()
// get them their address
s, err := n.ConsensusClient.CheckOut(nodeView.ID(), domain)
if err != nil {
log.Printf("HandleDNSQuery: consensus CheckOut error: %v\n", err)
return nil, err, true
}
addr, err := netip.ParseAddr(s)
if err != nil {
log.Printf("HandleDNSQuery: parse addr error: %v\n", err)
return nil, err, true
}
//make the msg to return
bs, err := dnsResponse(&msg, []netip.Addr{addr})
if err != nil {
log.Printf("HandleDNSQuery: generateDNSResponse error: %v\n", err)
return nil, err, true
}
return bs, nil, true
}
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
// TODO copied from natc.go - we have no TypeAAAA at the moment, will be broken in that case I guess
// dnsResponse makes a DNS response for the natc. If the dnsmessage is requesting TypeAAAA
// or TypeA the provided addrs of the requested type will be used.
func dnsResponse(req *dnsmessage.Message, addrs []netip.Addr) ([]byte, error) {
b := dnsmessage.NewBuilder(nil,
dnsmessage.Header{
ID: req.Header.ID,
Response: true,
Authoritative: true,
})
b.EnableCompression()
if len(req.Questions) == 0 {
return b.Finish()
}
q := req.Questions[0]
if err := b.StartQuestions(); err != nil {
return nil, err
}
if err := b.Question(q); err != nil {
return nil, err
}
if err := b.StartAnswers(); err != nil {
return nil, err
}
switch q.Type {
case dnsmessage.TypeAAAA, dnsmessage.TypeA:
want6 := q.Type == dnsmessage.TypeAAAA
for _, ip := range addrs {
if want6 != ip.Is6() {
continue
}
if want6 {
if err := b.AAAAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 5},
dnsmessage.AAAAResource{AAAA: ip.As16()},
); err != nil {
return nil, err
}
} else {
if err := b.AResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 5},
dnsmessage.AResource{A: ip.As4()},
); err != nil {
return nil, err
}
}
}
case dnsmessage.TypeSOA:
if err := b.SOAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
); err != nil {
return nil, err
}
case dnsmessage.TypeNS:
if err := b.NSResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.NSResource{NS: tsMBox},
); err != nil {
return nil, err
}
}
return b.Finish()
}
// TODO just copied straight from natc.go
func proxyTCPConn(c net.Conn, dest string) {
addrPortStr := c.LocalAddr().String()
_, port, err := net.SplitHostPort(addrPortStr)
if err != nil {
// TODO tcpRoundRobinHandler?
log.Printf("tcpRoundRobinHandler.Handle: bogus addrPort %q", addrPortStr)
c.Close()
return
}
p := &tcpproxy.Proxy{
ListenFunc: func(net, laddr string) (net.Listener, error) {
return netutil.NewOneConnListener(c, nil), nil
},
}
p.AddRoute(addrPortStr, &tcpproxy.DialProxy{
Addr: fmt.Sprintf("%s:%s", dest, port),
})
p.Start()
}
func (n *NatConnector) GetTCPHandlerForFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
nodeView, _, ok := n.whoIs("", src)
if !ok {
log.Printf("GetTCPHandlerForFlow: WhoIs invalid for: %v\n", src)
return nil, false // TODO ? correct?
}
from := nodeView.ID()
domain, err := n.ConsensusClient.LookupDomain(from, dst.Addr())
if err != nil {
log.Printf("GetTCPHandlerForFlow: LookupDomain error: %v\n", err)
return nil, true // TODO true?
}
// TODO if domain is empty I guess we return intercept false?
if domain == "" {
return nil, false
}
return func(conn net.Conn) {
proxyTCPConn(conn, domain)
}, true
}
func (n *NatConnector) Stop() {
fmt.Println("FRAN TODO Stop") // TODO fran
}
func (n *NatConnector) Start() {
}
func (n *NatConnector) StartConsensusMember(id string, clusterPeers tailcfg.ClusterInfo, varRoot string) {
var leaderAddress string
if clusterPeers.Leader.IsValid() {
leaderAddress = clusterPeers.Leader.String()
}
// TODO something to do with channels to stop this?
go func() {
n.logf("Starting ippool consensus membership for natc")
ippool.StartConsensusMember(id, clusterPeers.Addr.String(), leaderAddress, varRoot)
}()
n.ConsensusClient = ippool.NewConsensusClient(clusterPeers.Addr.String(), leaderAddress, n.logf)
}
func NewNatConnector(l logger.Logf, whoIs func(string, netip.AddrPort) (tailcfg.NodeView, tailcfg.UserProfile, bool)) NatConnector {
return NatConnector{logf: l, whoIs: whoIs}
}