summaryrefslogtreecommitdiffhomepage
path: root/cmd/natc/ippool.go
diff options
context:
space:
mode:
authorFran Bull <fran@tailscale.com>2024-09-30 10:27:39 -0700
committerFran Bull <fran@tailscale.com>2024-11-06 11:45:16 -0800
commit5f24261a1eee73b619e40b328ce20c709eff57df (patch)
tree8fc12810a09a90f1c9c7ed3812422e9b727bdb6b /cmd/natc/ippool.go
parent8dcbd988f7653aa17b33094d3f917125414aeab6 (diff)
downloadtailscale-fran/natc-raft.tar.xz
tailscale-fran/natc-raft.zip
Diffstat (limited to 'cmd/natc/ippool.go')
-rw-r--r--cmd/natc/ippool.go257
1 files changed, 257 insertions, 0 deletions
diff --git a/cmd/natc/ippool.go b/cmd/natc/ippool.go
new file mode 100644
index 000000000..091a93c87
--- /dev/null
+++ b/cmd/natc/ippool.go
@@ -0,0 +1,257 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/netip"
+ "sync"
+ "time"
+
+ "github.com/gaissmai/bart"
+ "tailscale.com/ipn/ipnstate"
+ "tailscale.com/syncs"
+ "tailscale.com/tailcfg"
+ "tailscale.com/tsnet"
+ "tailscale.com/util/mak"
+)
+
+type ipPool struct {
+ perPeerMap syncs.Map[tailcfg.NodeID, *perPeerState]
+ v4Ranges []netip.Prefix
+ dnsAddr netip.Addr
+ consensus *consensus
+}
+
+func (ipp *ipPool) DomainForIP(from tailcfg.NodeID, addr netip.Addr, updatedAt time.Time) string {
+ // TODO lock
+ pm, ok := ipp.perPeerMap.Load(from)
+ if !ok {
+ log.Printf("DomainForIP: peer state absent for: %d", from)
+ return ""
+ }
+ ww, ok := pm.AddrToDomain.Lookup(addr)
+ if !ok {
+ log.Printf("DomainForIP: peer state doesn't recognize domain")
+ return ""
+ }
+ go func() {
+ err := ipp.markLastUsed(from, addr, ww.Domain, updatedAt)
+ if err != nil {
+ panic(err)
+ }
+ }()
+ return ww.Domain
+}
+
+type markLastUsedArgs struct {
+ NodeID tailcfg.NodeID
+ Addr netip.Addr
+ Domain string
+ UpdatedAt time.Time
+}
+
+// called by raft
+func (cd *fsm) executeMarkLastUsed(bs []byte) commandResult {
+ var args markLastUsedArgs
+ err := json.Unmarshal(bs, &args)
+ if err != nil {
+ return commandResult{Err: err}
+ }
+ err = cd.applyMarkLastUsed(args.NodeID, args.Addr, args.Domain, args.UpdatedAt)
+ if err != nil {
+ return commandResult{Err: err}
+ }
+ return commandResult{}
+}
+
+func (ipp *fsm) applyMarkLastUsed(from tailcfg.NodeID, addr netip.Addr, domain string, updatedAt time.Time) error {
+ // TODO lock
+ ps, ok := ipp.perPeerMap.Load(from)
+ if !ok {
+ // unexpected in normal operation (but not an error?)
+ return nil
+ }
+ ww, ok := ps.AddrToDomain.Lookup(addr)
+ if !ok {
+ // unexpected in normal operation (but not an error?)
+ return nil
+ }
+ if ww.Domain != domain {
+ // then I guess we're too late to update lastUsed
+ return nil
+ }
+ if ww.LastUsed.After(updatedAt) {
+ // prefer the most recent
+ return nil
+ }
+ ww.LastUsed = updatedAt
+ ps.AddrToDomain.Insert(netip.PrefixFrom(addr, addr.BitLen()), ww)
+ return nil
+}
+
+func (ipp *ipPool) StartConsensus(peers []*ipnstate.PeerStatus, ts *tsnet.Server) {
+ v4, _ := ts.TailscaleIPs()
+ adminLn, err := ts.Listen("tcp", fmt.Sprintf("%s:6312", v4))
+ if err != nil {
+ log.Fatal(err)
+ }
+ raftLn, err := ts.Listen("tcp", fmt.Sprintf("%s:6311", v4))
+ if err != nil {
+ log.Fatal(err)
+ }
+ sl := StreamLayer{s: ts, Listener: raftLn}
+ lns := listeners{command: adminLn, raft: &sl}
+ cns, err := BootstrapConsensus((*fsm)(ipp), v4, &lns, peers, ts.HTTPClient())
+ if err != nil {
+ log.Fatalf("BootstrapConsensus failed: %v", err)
+ }
+ ipp.consensus = cns
+}
+
+type whereWhen struct {
+ Domain string
+ LastUsed time.Time
+}
+
+type perPeerState struct {
+ DomainToAddr map[string]netip.Addr
+ AddrToDomain *bart.Table[whereWhen]
+ mu sync.Mutex // not jsonified
+}
+
+func (ps *perPeerState) unusedIPV4(ranges []netip.Prefix, exclude netip.Addr, reuseDeadline time.Time) (netip.Addr, bool, string, error) {
+ // TODO here we iterate through each ip within the ranges until we find one that's unused
+ // could be done more efficiently either by:
+ // 1) storing an index into ranges and an ip we had last used from that range in perPeerState
+ // (how would this work with checking ips back into the pool though?)
+ // 2) using a random approach like the natc does now, except the raft state machine needs to
+ // be deterministic so it can replay logs, so I think we would do something like generate a
+ // random ip each time, and then have a call into the state machine that says "give me whatever
+ // ip you have, and if you don't have one use this one". I think that would work.
+ for _, r := range ranges {
+ ip := r.Addr()
+ for r.Contains(ip) {
+ if ip != exclude {
+ ww, ok := ps.AddrToDomain.Lookup(ip)
+ if !ok {
+ return ip, false, "", nil
+ }
+ if ww.LastUsed.Before(reuseDeadline) {
+ return ip, true, ww.Domain, nil
+ }
+ }
+ ip = ip.Next()
+ }
+ }
+ return netip.Addr{}, false, "", errors.New("ip pool exhausted")
+}
+
+func (cd *ipPool) IpForDomain(nid tailcfg.NodeID, domain string) (netip.Addr, error) {
+ now := time.Now()
+ args := checkoutAddrArgs{
+ NodeID: nid,
+ Domain: domain,
+ ReuseDeadline: now.Add(-10 * time.Second), // TODO what time period? 48 hours?
+ UpdatedAt: now,
+ }
+ bs, err := json.Marshal(args)
+ if err != nil {
+ return netip.Addr{}, err
+ }
+ c := command{
+ Name: "checkoutAddr",
+ Args: bs,
+ }
+ result, err := cd.consensus.executeCommand(c)
+ if err != nil {
+ log.Printf("IpForDomain: raft error executing command: %v", err)
+ return netip.Addr{}, err
+ }
+ if result.Err != nil {
+ log.Printf("IpForDomain: error returned from state machine: %v", err)
+ return netip.Addr{}, result.Err
+ }
+ var addr netip.Addr
+ err = json.Unmarshal(result.Result, &addr)
+ return addr, err
+}
+
+func (cd *ipPool) markLastUsed(nid tailcfg.NodeID, addr netip.Addr, domain string, lastUsed time.Time) error {
+ args := markLastUsedArgs{
+ NodeID: nid,
+ Addr: addr,
+ Domain: domain,
+ UpdatedAt: lastUsed,
+ }
+ bs, err := json.Marshal(args)
+ if err != nil {
+ return err
+ }
+ c := command{
+ Name: "markLastUsed",
+ Args: bs,
+ }
+ result, err := cd.consensus.executeCommand(c)
+ if err != nil {
+ log.Printf("markLastUsed: raft error executing command: %v", err)
+ return err
+ }
+ if result.Err != nil {
+ log.Printf("markLastUsed: error returned from state machine: %v", err)
+ return result.Err
+ }
+ return nil
+}
+
+type checkoutAddrArgs struct {
+ NodeID tailcfg.NodeID
+ Domain string
+ ReuseDeadline time.Time
+ UpdatedAt time.Time
+}
+
+// called by raft
+func (cd *fsm) executeCheckoutAddr(bs []byte) commandResult {
+ var args checkoutAddrArgs
+ err := json.Unmarshal(bs, &args)
+ if err != nil {
+ return commandResult{Err: err}
+ }
+ addr, err := cd.applyCheckoutAddr(args.NodeID, args.Domain, args.ReuseDeadline, args.UpdatedAt)
+ if err != nil {
+ return commandResult{Err: err}
+ }
+ resultBs, err := json.Marshal(addr)
+ if err != nil {
+ return commandResult{Err: err}
+ }
+ return commandResult{Result: resultBs}
+}
+
+func (cd *fsm) applyCheckoutAddr(nid tailcfg.NodeID, domain string, reuseDeadline, updatedAt time.Time) (netip.Addr, error) {
+ // TODO lock and unlock
+ pm, _ := cd.perPeerMap.LoadOrStore(nid, &perPeerState{
+ AddrToDomain: &bart.Table[whereWhen]{},
+ })
+ if existing, ok := pm.DomainToAddr[domain]; ok {
+ // TODO handle error case where this doesn't exist
+ ww, _ := pm.AddrToDomain.Lookup(existing)
+ ww.LastUsed = updatedAt
+ pm.AddrToDomain.Insert(netip.PrefixFrom(existing, existing.BitLen()), ww)
+ return existing, nil
+ }
+ addr, wasInUse, previousDomain, err := pm.unusedIPV4(cd.v4Ranges, cd.dnsAddr, reuseDeadline)
+ if err != nil {
+ return netip.Addr{}, err
+ }
+ mak.Set(&pm.DomainToAddr, domain, addr)
+ if wasInUse {
+ // remove it from domaintoaddr
+ delete(pm.DomainToAddr, previousDomain)
+ // don't need to remove it from addrtodomain, insert will do that
+ }
+ pm.AddrToDomain.Insert(netip.PrefixFrom(addr, addr.BitLen()), whereWhen{Domain: domain, LastUsed: updatedAt})
+ return addr, nil
+}