mirror of https://github.com/tailscale/tailscale/
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.
209 lines
5.9 KiB
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}
|
||
|
}
|