mirror of https://github.com/tailscale/tailscale/
wip
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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue