ipn/localapi: introduce get/set config for serve (#6243)

Updates tailscale/corp#7515

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
This commit is contained in:
shayne 2022-11-10 22:58:40 -05:00 committed by GitHub
parent 7b5866ac0a
commit e3a66e4d2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 112 additions and 10 deletions

View File

@ -859,6 +859,33 @@ type signRequest struct {
return nil return nil
} }
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
b, err := json.Marshal(&config)
if err != nil {
return fmt.Errorf("encoding config: %w", err)
}
_, err = lc.send(ctx, "POST", "/localapi/v0/serve-config", 200, bytes.NewReader(b))
if err != nil {
return fmt.Errorf("sending serve config: %w", err)
}
return nil
}
// GetServeConfig return the current serve config.
func (lc *LocalClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/serve-config", 200, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
}
sc := new(ipn.ServeConfig)
if err := json.Unmarshal(body, sc); err != nil {
return nil, err
}
return sc, nil
}
// tailscaledConnectHint gives a little thing about why tailscaled (or // tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections. // platform equivalent) is not answering localapi connections.
// //

View File

@ -65,7 +65,7 @@ func (src *ServeConfig) Clone() *ServeConfig {
dst := new(ServeConfig) dst := new(ServeConfig)
*dst = *src *dst = *src
if dst.TCP != nil { if dst.TCP != nil {
dst.TCP = map[int]*TCPPortHandler{} dst.TCP = map[uint16]*TCPPortHandler{}
for k, v := range src.TCP { for k, v := range src.TCP {
dst.TCP[k] = v.Clone() dst.TCP[k] = v.Clone()
} }
@ -87,7 +87,7 @@ func (src *ServeConfig) Clone() *ServeConfig {
// A compilation failure here means this code must be regenerated, with the command at the top of this file. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct { var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
TCP map[int]*TCPPortHandler TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig Web map[HostPort]*WebServerConfig
AllowIngress map[HostPort]bool AllowIngress map[HostPort]bool
}{}) }{})

View File

@ -164,7 +164,7 @@ func (v *ServeConfigView) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (v ServeConfigView) TCP() views.MapFn[int, *TCPPortHandler, TCPPortHandlerView] { func (v ServeConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandlerView] {
return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView { return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView {
return t.View() return t.View()
}) })
@ -182,7 +182,7 @@ func (v ServeConfigView) AllowIngress() views.Map[HostPort, bool] {
// A compilation failure here means this code must be regenerated, with the command at the top of this file. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct { var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
TCP map[int]*TCPPortHandler TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig Web map[HostPort]*WebServerConfig
AllowIngress map[HostPort]bool AllowIngress map[HostPort]bool
}{}) }{})

View File

@ -11,7 +11,6 @@
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
@ -3522,8 +3521,8 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked() {
} }
} }
if b.serveConfig.Valid() { if b.serveConfig.Valid() {
b.serveConfig.TCP().Range(func(port int, _ ipn.TCPPortHandlerView) bool { b.serveConfig.TCP().Range(func(port uint16, _ ipn.TCPPortHandlerView) bool {
if port > 0 && port <= math.MaxUint16 { if port > 0 {
handlePorts = append(handlePorts, uint16(port)) handlePorts = append(handlePorts, uint16(port))
} }
return true return true
@ -3534,6 +3533,46 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked() {
b.setTCPPortsIntercepted(handlePorts) b.setTCPPortsIntercepted(handlePorts)
} }
// SetServeConfig establishes or replaces the current serve config.
func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
nm := b.netMap
if nm == nil {
return errors.New("netMap is nil")
}
if nm.SelfNode == nil {
return errors.New("netMap SelfNode is nil")
}
profileID := fmt.Sprintf("node-%s", nm.SelfNode.StableID) // TODO(maisem,bradfitz): something else?
confKey := ipn.ServeConfigKey(profileID)
var bs []byte
if config != nil {
j, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("encoding serve config: %w", err)
}
bs = j
}
if err := b.store.WriteState(confKey, bs); err != nil {
return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
}
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked()
return nil
}
// ServeConfig provides a view of the current serve mappings.
// If serving is not configured, the returned view is not Valid.
func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
b.mu.Lock()
defer b.mu.Unlock()
return b.serveConfig
}
// operatorUserName returns the current pref's OperatorUser's name, or the // operatorUserName returns the current pref's OperatorUser's name, or the
// empty string if none. // empty string if none.
func (b *LocalBackend) operatorUserName() string { func (b *LocalBackend) operatorUserName() string {

View File

@ -82,7 +82,7 @@ func (b *LocalBackend) HandleInterceptedTCPConn(dport uint16, srcAddr netip.Addr
return return
} }
tcph, ok := sc.TCP().GetOk(int(dport)) tcph, ok := sc.TCP().GetOk(dport)
if !ok { if !ok {
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr) b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
sendRST() sendRST()

View File

@ -72,6 +72,7 @@
"ping": (*Handler).servePing, "ping": (*Handler).servePing,
"prefs": (*Handler).servePrefs, "prefs": (*Handler).servePrefs,
"pprof": (*Handler).servePprof, "pprof": (*Handler).servePprof,
"serve-config": (*Handler).serveServeConfig,
"set-dns": (*Handler).serveSetDNS, "set-dns": (*Handler).serveSetDNS,
"set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-expiry-sooner": (*Handler).serveSetExpirySooner,
"status": (*Handler).serveStatus, "status": (*Handler).serveStatus,
@ -455,6 +456,41 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
servePprofFunc(w, r) servePprofFunc(w, r)
} }
func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "serve config denied", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
config := h.b.ServeConfig()
json.NewEncoder(w).Encode(config)
case "POST":
configIn := new(ipn.ServeConfig)
if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
json.NewEncoder(w).Encode(struct {
Error error
}{
Error: fmt.Errorf("decoding config: %w", err),
})
return
}
err := h.b.SetServeConfig(configIn)
if err != nil {
json.NewEncoder(w).Encode(struct {
Error error
}{
Error: fmt.Errorf("updating config: %w", err),
})
return
}
w.WriteHeader(http.StatusOK)
}
}
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead { if !h.PermitRead {
http.Error(w, "IP forwarding check access denied", http.StatusForbidden) http.Error(w, "IP forwarding check access denied", http.StatusForbidden)

View File

@ -76,7 +76,7 @@ func ServeConfigKey(profileID string) StateKey {
type ServeConfig struct { type ServeConfig struct {
// TCP are the list of TCP port numbers that tailscaled should handle for // TCP are the list of TCP port numbers that tailscaled should handle for
// the Tailscale IP addresses. (not subnet routers, etc) // the Tailscale IP addresses. (not subnet routers, etc)
TCP map[int]*TCPPortHandler `json:",omitempty"` TCP map[uint16]*TCPPortHandler `json:",omitempty"`
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers // Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
// keyed by mount point ("/", "/foo", etc) // keyed by mount point ("/", "/foo", etc)
@ -84,7 +84,7 @@ type ServeConfig struct {
// AllowIngress is the set of SNI:port values for which ingress // AllowIngress is the set of SNI:port values for which ingress
// traffic is allowed, from trusted ingress peers. // traffic is allowed, from trusted ingress peers.
AllowIngress map[HostPort]bool AllowIngress map[HostPort]bool `json:",omitempty"`
} }
// HostPort is an SNI name and port number, joined by a colon. // HostPort is an SNI name and port number, joined by a colon.