Fran Bull 5 days ago
parent d349370e55
commit 9bb4dbb1dd

@ -0,0 +1,116 @@
package appc
import (
"net/netip"
"sync"
"tailscale.com/tailcfg"
)
type Conn25 struct {
mu sync.Mutex
transitIPMap map[tailcfg.NodeID]map[netip.Addr]netip.Addr
}
func (c *Conn25) HandleConnectorTransitIPRequest(nid tailcfg.NodeID, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse {
resp := ConnectorTransitIPResponse{}
for _, each := range ctipr.TransitIPs {
tipresp := c.handleTransitIPRequest(nid, each)
resp.TransitIPs = append(resp.TransitIPs, tipresp)
}
return resp
}
func (c *Conn25) handleTransitIPRequest(nid tailcfg.NodeID, tipr TransitIPRequest) TransitIPResponse {
c.mu.Lock()
defer c.mu.Unlock()
if c.transitIPMap == nil {
c.transitIPMap = make(map[tailcfg.NodeID]map[netip.Addr]netip.Addr)
}
peerMap, ok := c.transitIPMap[nid]
if !ok {
peerMap = make(map[netip.Addr]netip.Addr)
c.transitIPMap[nid] = peerMap
}
peerMap[tipr.TransitIP] = tipr.DestinationIP
return TransitIPResponse{}
}
// TransitIPRequest details a single TransitIP allocation request from a client to a
// connector.
type TransitIPRequest struct {
// Mapping details
// TransitIP is the intermediate destination IP that will be received at this
// connector and will be replaced by DestinationIP when performing DNAT. The
// TransitIP is specific to the peer making this request and must be within the
// tailnet's TransitIP range.
// If the connector already has a mapping for this TransitIP from this client, it
// will be replaced with the new mapping specified here. It is an error to request
// the same TransitIP more than once in the same [ConnectorTransitIPRequest].
TransitIP netip.Addr `json:"transitIP,omitzero"`
// DestinationIP is the final destination IP that connections to the TransitIP
// should be mapped to when performing DNAT.
// If the connector already has a mapping for this DestinationIP in the context of
// this client and App, then the connector may immediately expire the old mapping.
DestinationIP netip.Addr `json:"destinationIP,omitzero"`
// AppName is the name of the connector application from the tailnet
// configuration, as listed in [appctype.AppConnectorAttr.Name].
App string `json:"app,omitzero"`
// Proof of destination IP (optional)
// FQDNs is an optional list of FQDNs that have previously resolved to the
// requested destination IP. If the connector's destination IP cache does not
// currently indicate that the destination IP applies to the app, then the
// connector may attempt DNS resolution to confirm the destination IP instead of
// rejecting the request.
FQDNs []string `json:"fqdns,omitempty"`
}
// ConnectorTransitIPRequest is the request body for a PeerAPI request to
// /connector/transit-ip and can include zero or more TransitIP allocation requests.
type ConnectorTransitIPRequest struct {
// Clear is set when the client wishes to flush the connector's TransitIP
// configuration for this client. The connector may ignore Clear, for it is simply
// a hint to accelerate cache expiry. Clients are expected to set this on the
// first request to a particular connector after client start in order to clear
// lingering mappings from a prior instance.
Clear bool `json:"clear,omitzero"`
// TransitIPs is the list of requested mappings.
TransitIPs []TransitIPRequest `json:"transitIPs,omitempty"`
}
type TransitIPResponseCode int
const (
// OK indicates that the mapping was created as requested.
OK TransitIPResponseCode = 0
// OtherFailure indicates that the mapping failed for a reason that does not have
// another relevant [TransitIPResponsecode].
OtherFailure TransitIPResponseCode = 1
// MissingProof indicates that the mapping failed because the connector has not
// seen sufficient proof (via local cache or the Proof section of
// [TransitIPRequest]) that the requested destination IP applies to the specified
// App. The request can be retried after supplying additional proof.
MissingProof TransitIPResponseCode = 2
)
type TransitIPResponse struct {
// Code is an error code indicating success or failure of the [TransitIPRequest].
Code TransitIPResponseCode `json:"code,omitzero"`
// Message is an error message explaining what happened, suitable for logging but
// not necessarily suitable for displaying in a UI to non-technical users. It
// should be empty when [Code] is [OK].
Message string `json:"message,omitzero"`
}
type ConnectorTransitIPResponse struct {
// Clear is set when the connector wishes to flush the client's TransitIP
// configuration for this connector. The client may ignore Clear, for it is simply
// a hint to accelerate cache expiry. Connectors are expected to set this on the
// first response to a particular client after connector start in order to clear
// lingering mappings from a prior instance.
Clear bool `json:"clear,omitzero"`
// TransitIPs is the list of outcomes for each requested mapping. Elements
// correspond to the order of [ConnectorTransitIPRequest.TransitIPs].
TransitIPs []TransitIPResponse `json:"transitIPs,omitempty"`
}

@ -0,0 +1,53 @@
package appc
import (
"net/netip"
"testing"
"tailscale.com/tailcfg"
)
func TestHandleConnectorTransitIPRequest(t *testing.T) {
c := &Conn25{}
// specific to the peer making this request and
// must be within the tailnet's TransitIP range.
// If the connector already has a mapping for this TransitIP from this client, it will be replaced with the new mapping specified here.
// It is an error to request the same TransitIP more than once in the same [ConnectorTransitIPRequest].
req := ConnectorTransitIPRequest{}
nid := tailcfg.NodeID(1)
resp := c.HandleConnectorTransitIPRequest(nid, req)
if len(resp.TransitIPs) != 0 {
t.Fatal("shoulda been 0")
}
tip := netip.MustParseAddr("0.0.0.1")
dip := netip.MustParseAddr("1.2.3.4")
req = ConnectorTransitIPRequest{
TransitIPs: []TransitIPRequest{
{TransitIP: tip, DestinationIP: dip},
},
}
resp = c.HandleConnectorTransitIPRequest(tailcfg.NodeID(1), req)
if len(resp.TransitIPs) != 1 {
t.Fatal("shoulda been 1")
}
if resp.TransitIPs[0].Code != TransitIPResponseCode(0) {
t.Fatal("shoulda been 0")
}
func() {
c.mu.Lock()
defer c.mu.Unlock()
state, ok := c.transitIPMap[nid]
if !ok {
t.Fatal("shoulda found it")
}
stored, ok := state[tip]
if !ok {
t.Fatal("shoulda found it")
}
if stored != dip {
t.Fatal("shoulda been dip")
}
}()
}

@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_conn25
package condregister
import _ "tailscale.com/feature/conn25"

@ -0,0 +1,70 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package conn25 registers the conn25 feature and implements its associated ipnext.Extension.
package conn25
import (
"encoding/json"
"net/http"
"tailscale.com/appc"
"tailscale.com/feature"
"tailscale.com/ipn/ipnext"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/types/logger"
)
// featureName is the name of the feature implemented by this package.
// It is also the [extension] name and the log prefix.
const featureName = "conn25"
func init() {
feature.Register(featureName)
newExtension := func(logf logger.Logf, sb ipnext.SafeBackend) (ipnext.Extension, error) {
e := &extension{
conn: &appc.Conn25{},
}
ipnlocal.RegisterPeerAPIHandler("/v0/connector/transit-ip/", e.handleConnectorTransitIP)
return e, nil
}
ipnext.RegisterExtension(featureName, newExtension)
}
// extension is an [ipnext.Extension] managing the relay server on platforms
// that import this package.
type extension struct {
conn *appc.Conn25
}
// Name implements [ipnext.Extension].
func (e *extension) Name() string {
return featureName
}
// Init implements [ipnext.Extension].
func (e *extension) Init(host ipnext.Host) error {
return nil
}
// Shutdown implements [ipnlocal.Extension].
func (e *extension) Shutdown() error {
return nil
}
func (e *extension) handleConnectorTransitIP(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
var req appc.ConnectorTransitIPRequest
defer r.Body.Close()
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Error decoding JSON", http.StatusBadRequest)
return
}
resp := e.conn.HandleConnectorTransitIPRequest(h.Peer().ID(), req)
bs, err := json.Marshal(resp)
if err != nil {
http.Error(w, "Error encoding JSON", http.StatusInternalServerError)
return
}
w.Write(bs)
}

@ -6,6 +6,7 @@
package ipnlocal
import (
"bytes"
"cmp"
"context"
"crypto/sha256"
@ -5028,7 +5029,6 @@ func (b *LocalBackend) authReconfig() {
//
// b.mu must be held.
func (b *LocalBackend) authReconfigLocked() {
if b.shutdownCalled {
b.logf("[v1] authReconfig: skipping because in shutdown")
return
@ -5053,7 +5053,6 @@ func (b *LocalBackend) authReconfigLocked() {
dcfg := cn.dnsConfigForNetmap(prefs, b.keyExpired, version.OS())
// If the current node is an app connector, ensure the app connector machine is started
b.reconfigAppConnectorLocked(nm, prefs)
if !prefs.WantRunning() {
b.logf("[v1] authReconfig: skipping because !WantRunning.")
return
@ -5573,6 +5572,47 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
}
}
func doDebugThing(b *LocalBackend) {
client := &http.Client{
Transport: b.Dialer().PeerAPITransport(),
Timeout: 10 * time.Second,
}
peers := b.NodeBackend().AppendMatchingPeers(nil, func(nv tailcfg.NodeView) bool {
return nv.Name() == "8feb8d2ec80f.taile25f.ts.net."
})
dstURL := b.NodeBackend().PeerAPIBase(peers[0])
ctx := context.Background()
r := appc.ConnectorTransitIPRequest{
TransitIPs: []appc.TransitIPRequest{
{TransitIP: netip.MustParseAddr("1.1.1.1"), DestinationIP: netip.MustParseAddr("2.1.1.1")},
{TransitIP: netip.MustParseAddr("1.1.1.2"), DestinationIP: netip.MustParseAddr("2.1.1.2")},
},
}
rbs, err := json.Marshal(r)
if err != nil {
fmt.Println(err)
}
req, err := http.NewRequestWithContext(ctx, "GET", dstURL+"/v0/connector/transit-ip/", bytes.NewBuffer(rbs))
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
panic(err)
}
if resp != nil {
fmt.Println("doDebugThing resp.Status:", resp.Status)
bs, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println("doDebugThning resp.body:", string(bs))
}
}
// enterStateLocked transitions the backend into newState, updating internal
// state and propagating events out as needed.
//
@ -5677,6 +5717,9 @@ func (b *LocalBackend) enterStateLocked(newState ipn.State) {
}
feature.SystemdStatus("Connected; %s; %s", activeLogin, strings.Join(addrStrs, " "))
}
if b.currentNode().Self().Name() == "d783302cc665.taile25f.ts.net." {
doDebugThing(b)
}
default:
b.logf("[unexpected] unknown newState %#v", newState)
}

Loading…
Cancel
Save