Compare commits
2 Commits
webnet
..
cef1031b8e
| Author | SHA1 | Date | |
|---|---|---|---|
| cef1031b8e | |||
| 13e9529084 |
@@ -1,301 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & contributors
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !ts_omit_drive
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"syscall/js"
|
|
||||||
|
|
||||||
"tailscale.com/drive"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
"tailscale.com/tsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Compile-time check that jsFileSystemForRemote implements drive.FileSystemForRemote.
|
|
||||||
var _ drive.FileSystemForRemote = (*jsFileSystemForRemote)(nil)
|
|
||||||
|
|
||||||
// jsFileSystemForRemote implements drive.FileSystemForRemote by bridging
|
|
||||||
// incoming WebDAV requests to a JS handler function. Auth and permission
|
|
||||||
// parsing are handled upstream by handleServeDrive before this is called.
|
|
||||||
type jsFileSystemForRemote struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
fn js.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *jsFileSystemForRemote) setHandler(fn js.Value) {
|
|
||||||
fs.mu.Lock()
|
|
||||||
fs.fn = fn
|
|
||||||
fs.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFileServerAddr is a no-op: the JS handler owns its own storage.
|
|
||||||
func (fs *jsFileSystemForRemote) SetFileServerAddr(_ string) {}
|
|
||||||
|
|
||||||
// SetShares is a no-op: the JS handler controls which shares it exposes.
|
|
||||||
func (fs *jsFileSystemForRemote) SetShares(_ []*drive.Share) {}
|
|
||||||
|
|
||||||
// Close is a no-op.
|
|
||||||
func (fs *jsFileSystemForRemote) Close() error { return nil }
|
|
||||||
|
|
||||||
// ServeHTTPWithPerms handles a WebDAV request by bridging it to the JS handler.
|
|
||||||
// It streams the request body to JS via readBodyChunk() and streams the
|
|
||||||
// response body back via write()/end() callbacks, so no full-body buffering
|
|
||||||
// occurs regardless of file size.
|
|
||||||
//
|
|
||||||
// The call blocks until JS calls end() (or a write error occurs).
|
|
||||||
func (fs *jsFileSystemForRemote) ServeHTTPWithPerms(
|
|
||||||
perms drive.Permissions, w http.ResponseWriter, r *http.Request,
|
|
||||||
) {
|
|
||||||
fs.mu.RLock()
|
|
||||||
fn := fs.fn
|
|
||||||
fs.mu.RUnlock()
|
|
||||||
|
|
||||||
if fn.IsUndefined() || fn.IsNull() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// readBodyChunk is exposed to JS as req.readBodyChunk().
|
|
||||||
// Each call returns a Promise<Uint8Array|null>: null signals EOF.
|
|
||||||
readBodyChunk := js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
|
||||||
return makePromise(func() (any, error) {
|
|
||||||
buf := make([]byte, 65536)
|
|
||||||
n, err := r.Body.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
arr := js.Global().Get("Uint8Array").New(n)
|
|
||||||
js.CopyBytesToJS(arr, buf[:n])
|
|
||||||
return arr, nil
|
|
||||||
}
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
return js.Null(), nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// doneCh receives nil when JS calls end(), or a write error if Write fails.
|
|
||||||
doneCh := make(chan error, 1)
|
|
||||||
|
|
||||||
// writeHead sets response headers and status code. Must be called before write().
|
|
||||||
writeHead := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
|
||||||
if len(args) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
status := args[0].Int()
|
|
||||||
if len(args) > 1 && !args[1].IsUndefined() && !args[1].IsNull() {
|
|
||||||
for k, vs := range jsHeadersToGo(args[1]) {
|
|
||||||
for _, v := range vs {
|
|
||||||
w.Header().Add(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.WriteHeader(status)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// write streams a single response body chunk to the client.
|
|
||||||
write := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
|
||||||
if len(args) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data := args[0]
|
|
||||||
buf := make([]byte, data.Get("length").Int())
|
|
||||||
js.CopyBytesToGo(buf, data)
|
|
||||||
if _, werr := w.Write(buf); werr != nil {
|
|
||||||
select {
|
|
||||||
case doneCh <- werr:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if f, ok := w.(http.Flusher); ok {
|
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// end signals that the response is complete.
|
|
||||||
end := js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
|
||||||
select {
|
|
||||||
case doneCh <- nil:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
readBodyChunk.Release()
|
|
||||||
writeHead.Release()
|
|
||||||
write.Release()
|
|
||||||
end.Release()
|
|
||||||
}()
|
|
||||||
|
|
||||||
jsReq := map[string]any{
|
|
||||||
"method": r.Method,
|
|
||||||
"path": r.URL.Path,
|
|
||||||
"rawQuery": r.URL.RawQuery,
|
|
||||||
"headers": goHeadersToJS(r.Header),
|
|
||||||
"readBodyChunk": readBodyChunk,
|
|
||||||
}
|
|
||||||
jsRes := map[string]any{
|
|
||||||
"writeHead": writeHead,
|
|
||||||
"write": write,
|
|
||||||
"end": end,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn.Invoke(jsReq, jsRes, drivePermsToJS(perms))
|
|
||||||
|
|
||||||
// Block this goroutine until JS calls end() or a write error occurs.
|
|
||||||
// The Go WASM scheduler yields back to JS while we wait.
|
|
||||||
<-doneCh
|
|
||||||
}
|
|
||||||
|
|
||||||
// drivePermsToJS converts drive.Permissions to a plain JS-friendly object.
|
|
||||||
// Each share name maps to a numeric permission: 0=none, 1=read-only, 2=read-write.
|
|
||||||
// The wildcard share name "*" is included if present.
|
|
||||||
func drivePermsToJS(p drive.Permissions) map[string]any {
|
|
||||||
result := make(map[string]any, len(p))
|
|
||||||
for name, perm := range p {
|
|
||||||
result[name] = int(perm)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// goHeadersToJS converts an http.Header to a map[string]any suitable for JS.
|
|
||||||
// Single-value headers become a string; multi-value headers become a []any.
|
|
||||||
func goHeadersToJS(h http.Header) map[string]any {
|
|
||||||
result := make(map[string]any, len(h))
|
|
||||||
for k, vs := range h {
|
|
||||||
if len(vs) == 1 {
|
|
||||||
result[k] = vs[0]
|
|
||||||
} else {
|
|
||||||
arr := make([]any, len(vs))
|
|
||||||
for i, v := range vs {
|
|
||||||
arr[i] = v
|
|
||||||
}
|
|
||||||
result[k] = arr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// jsHeadersToGo parses a JS headers object into an http.Header map.
|
|
||||||
// Values may be a string or an array of strings.
|
|
||||||
func jsHeadersToGo(jsHeaders js.Value) http.Header {
|
|
||||||
h := make(http.Header)
|
|
||||||
keys := js.Global().Get("Object").Call("keys", jsHeaders)
|
|
||||||
for i := 0; i < keys.Length(); i++ {
|
|
||||||
key := keys.Index(i).String()
|
|
||||||
val := jsHeaders.Get(key)
|
|
||||||
switch val.Type() {
|
|
||||||
case js.TypeString:
|
|
||||||
h.Set(key, val.String())
|
|
||||||
case js.TypeObject:
|
|
||||||
if val.InstanceOf(js.Global().Get("Array")) {
|
|
||||||
for j := 0; j < val.Length(); j++ {
|
|
||||||
h.Add(key, val.Index(j).String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// initDriveForRemote creates the JS-backed FileSystemForRemote and registers
|
|
||||||
// it with sys. Must be called before NewLocalBackend (SubSystem is set-once).
|
|
||||||
func initDriveForRemote(sys *tsd.System) *jsFileSystemForRemote {
|
|
||||||
driveFS := &jsFileSystemForRemote{}
|
|
||||||
sys.Set(driveFS)
|
|
||||||
return driveFS
|
|
||||||
}
|
|
||||||
|
|
||||||
// wireDriveJS adds drive-related methods to the IPN JS methods map.
|
|
||||||
// driveFS must be the value returned by initDriveForRemote.
|
|
||||||
func wireDriveJS(i *jsIPN, driveFS *jsFileSystemForRemote, m map[string]any) {
|
|
||||||
m["setDriveHandler"] = js.FuncOf(func(_ js.Value, args []js.Value) any {
|
|
||||||
if len(args) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
driveFS.setHandler(args[0])
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
m["listDrivePeers"] = js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
|
||||||
return i.listDrivePeers()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type jsDrivePeer struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
PeerAPIURL string `json:"peerAPIURL"`
|
|
||||||
StableNodeID string `json:"stableNodeID"`
|
|
||||||
Online *bool `json:"online,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// listDrivePeers returns a JSON array of peers that carry
|
|
||||||
// PeerCapabilityTaildriveSharer. Returns an empty array if the local node
|
|
||||||
// does not have drive:access in its ACL (DriveAccessEnabled). This mirrors
|
|
||||||
// the filtering in LocalBackend.driveRemotesFromPeers.
|
|
||||||
func (i *jsIPN) listDrivePeers() js.Value {
|
|
||||||
return makePromise(func() (any, error) {
|
|
||||||
if !i.lb.DriveAccessEnabled() {
|
|
||||||
return "[]", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nm := i.lb.NetMap()
|
|
||||||
if nm == nil {
|
|
||||||
return nil, errors.New("listDrivePeers: no network map available")
|
|
||||||
}
|
|
||||||
|
|
||||||
var selfHave4, selfHave6 bool
|
|
||||||
for _, a := range nm.GetAddresses().All() {
|
|
||||||
if !a.IsSingleIP() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if a.Addr().Is4() {
|
|
||||||
selfHave4 = true
|
|
||||||
} else if a.Addr().Is6() {
|
|
||||||
selfHave6 = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peers := make([]jsDrivePeer, 0)
|
|
||||||
for _, p := range nm.Peers {
|
|
||||||
// Check PeerCapabilityTaildriveSharer via the live PeerCaps map
|
|
||||||
// (derived from ACL rules), mirroring driveRemotesFromPeers.
|
|
||||||
hasCap := false
|
|
||||||
for _, a := range p.Addresses().All() {
|
|
||||||
if a.IsSingleIP() && i.lb.PeerCaps(a.Addr()).HasCapability(tailcfg.PeerCapabilityTaildriveSharer) {
|
|
||||||
hasCap = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasCap {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
peerURL := buildPeerAPIURL(p, selfHave4, selfHave6)
|
|
||||||
online := p.Online().Clone()
|
|
||||||
peers = append(peers, jsDrivePeer{
|
|
||||||
Name: p.DisplayName(false),
|
|
||||||
PeerAPIURL: peerURL,
|
|
||||||
StableNodeID: string(p.StableID()),
|
|
||||||
Online: online,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(peers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("listDrivePeers: marshal: %w", err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & contributors
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build ts_omit_drive
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall/js"
|
|
||||||
|
|
||||||
"tailscale.com/tsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
type jsFileSystemForRemote struct{}
|
|
||||||
|
|
||||||
// initDriveForRemote is a no-op when the drive feature is omitted.
|
|
||||||
func initDriveForRemote(_ *tsd.System) *jsFileSystemForRemote { return nil }
|
|
||||||
|
|
||||||
// wireDriveJS is a no-op when the drive feature is omitted.
|
|
||||||
func wireDriveJS(_ *jsIPN, _ *jsFileSystemForRemote, _ map[string]any) {}
|
|
||||||
|
|
||||||
// listDrivePeers returns an empty list when the drive feature is omitted.
|
|
||||||
func (i *jsIPN) listDrivePeers() js.Value {
|
|
||||||
return makePromise(func() (any, error) {
|
|
||||||
return "[]", nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & contributors
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// buildPeerAPIURL returns the HTTP base URL for a peer's peerAPI server,
|
|
||||||
// selecting IPv4 when available and falling back to IPv6. Returns an empty
|
|
||||||
// string if the peer advertises no reachable peerAPI port.
|
|
||||||
func buildPeerAPIURL(p tailcfg.NodeView, selfHave4, selfHave6 bool) string {
|
|
||||||
var pp4, pp6 uint16
|
|
||||||
for _, s := range p.Hostinfo().Services().All() {
|
|
||||||
switch s.Proto {
|
|
||||||
case tailcfg.PeerAPI4:
|
|
||||||
pp4 = s.Port
|
|
||||||
case tailcfg.PeerAPI6:
|
|
||||||
pp6 = s.Port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if selfHave4 && pp4 != 0 {
|
|
||||||
for _, a := range p.Addresses().All() {
|
|
||||||
if a.IsSingleIP() && a.Addr().Is4() {
|
|
||||||
return fmt.Sprintf("http://%v", netip.AddrPortFrom(a.Addr(), pp4))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if selfHave6 && pp6 != 0 {
|
|
||||||
for _, a := range p.Addresses().All() {
|
|
||||||
if a.IsSingleIP() && a.Addr().Is6() {
|
|
||||||
return fmt.Sprintf("http://%v", netip.AddrPortFrom(a.Addr(), pp6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -66,20 +66,19 @@ import (
|
|||||||
var ControlURL = ipn.DefaultControlURL
|
var ControlURL = ipn.DefaultControlURL
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
shutdownCh := make(chan struct{})
|
|
||||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
|
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
log.Fatal("Usage: newIPN(config)")
|
log.Fatal("Usage: newIPN(config)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return newIPN(args[0], shutdownCh)
|
return newIPN(args[0])
|
||||||
}))
|
}))
|
||||||
// Block until shutdown() is called on the IPN, then let main return so the
|
// Keep Go runtime alive, otherwise it will be shut down before newIPN gets
|
||||||
// Go runtime (and all its goroutines) can be collected by the JS engine.
|
// called.
|
||||||
<-shutdownCh
|
<-make(chan bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any {
|
func newIPN(jsConfig js.Value) map[string]any {
|
||||||
netns.SetEnabled(false)
|
netns.SetEnabled(false)
|
||||||
|
|
||||||
var store ipn.StateStore
|
var store ipn.StateStore
|
||||||
@@ -159,10 +158,6 @@ func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any {
|
|||||||
sys.Tun.Get().Start()
|
sys.Tun.Get().Start()
|
||||||
|
|
||||||
logid := lpc.PublicID
|
logid := lpc.PublicID
|
||||||
|
|
||||||
// initDriveForRemote must be called before NewLocalBackend (SubSystem is set-once).
|
|
||||||
driveFS := initDriveForRemote(sys)
|
|
||||||
|
|
||||||
srv := ipnserver.New(logf, logid, sys.Bus.Get(), sys.NetMon.Get())
|
srv := ipnserver.New(logf, logid, sys.Bus.Get(), sys.NetMon.Get())
|
||||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
|
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -184,11 +179,10 @@ func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any {
|
|||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
logID: logid,
|
logID: logid,
|
||||||
funnelPorts: make(map[uint16]*funnelListenerEntry),
|
funnelPorts: make(map[uint16]*funnelListenerEntry),
|
||||||
shutdownCh: shutdownCh,
|
|
||||||
}
|
}
|
||||||
lb.SetTCPHandlerForFunnelFlow(jsIPN.handleFunnelTCP)
|
lb.SetTCPHandlerForFunnelFlow(jsIPN.handleFunnelTCP)
|
||||||
|
|
||||||
m := map[string]any{
|
return map[string]any{
|
||||||
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
|
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
log.Fatal(`Usage: run({
|
log.Fatal(`Usage: run({
|
||||||
@@ -367,16 +361,6 @@ func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any {
|
|||||||
"suggestExitNode": js.FuncOf(func(this js.Value, args []js.Value) any {
|
"suggestExitNode": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
return jsIPN.suggestExitNode()
|
return jsIPN.suggestExitNode()
|
||||||
}),
|
}),
|
||||||
"shutdown": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
||||||
return jsIPN.shutdown()
|
|
||||||
}),
|
|
||||||
"setServices": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
||||||
if len(args) != 1 {
|
|
||||||
log.Printf("Usage: setServices(services)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return jsIPN.setServices(args[0])
|
|
||||||
}),
|
|
||||||
"localAPI": js.FuncOf(func(this js.Value, args []js.Value) any {
|
"localAPI": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
log.Printf("Usage: localAPI(method, path[, body])")
|
log.Printf("Usage: localAPI(method, path[, body])")
|
||||||
@@ -389,8 +373,6 @@ func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any {
|
|||||||
return jsIPN.localAPI(args[0].String(), args[1].String(), body)
|
return jsIPN.localAPI(args[0].String(), args[1].String(), body)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
wireDriveJS(jsIPN, driveFS, m)
|
|
||||||
return m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsIPN struct {
|
type jsIPN struct {
|
||||||
@@ -405,12 +387,6 @@ type jsIPN struct {
|
|||||||
|
|
||||||
funnelMu sync.Mutex
|
funnelMu sync.Mutex
|
||||||
funnelPorts map[uint16]*funnelListenerEntry
|
funnelPorts map[uint16]*funnelListenerEntry
|
||||||
|
|
||||||
// ln is the safesocket listener created by run(); stored here so shutdown
|
|
||||||
// can close it and unblock srv.Run.
|
|
||||||
ln net.Listener
|
|
||||||
shutdownCh chan struct{} // closed by shutdown() to unblock main()
|
|
||||||
shutdownOnce sync.Once
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// funnelListenerEntry is the per-port state for routing Funnel connections to a listenTLS listener.
|
// funnelListenerEntry is the per-port state for routing Funnel connections to a listenTLS listener.
|
||||||
@@ -491,7 +467,6 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
|||||||
NodeKey: nm.NodeKey.String(),
|
NodeKey: nm.NodeKey.String(),
|
||||||
MachineKey: nm.MachineKey.String(),
|
MachineKey: nm.MachineKey.String(),
|
||||||
PeerAPIURL: selfPeerAPIURL,
|
PeerAPIURL: selfPeerAPIURL,
|
||||||
Services: userServicesFromView(nm.SelfNode.Hostinfo().Services()),
|
|
||||||
},
|
},
|
||||||
MachineStatus: jsMachineStatus[nm.GetMachineStatus()],
|
MachineStatus: jsMachineStatus[nm.GetMachineStatus()],
|
||||||
},
|
},
|
||||||
@@ -507,7 +482,32 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Peer peerAPI URL from the peer's advertised Services.
|
// Peer peerAPI URL from the peer's advertised Services.
|
||||||
peerURL := buildPeerAPIURL(p, selfHave4, selfHave6)
|
peerURL := ""
|
||||||
|
var pp4, pp6 uint16
|
||||||
|
for _, s := range p.Hostinfo().Services().All() {
|
||||||
|
switch s.Proto {
|
||||||
|
case tailcfg.PeerAPI4:
|
||||||
|
pp4 = s.Port
|
||||||
|
case tailcfg.PeerAPI6:
|
||||||
|
pp6 = s.Port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selfHave4 && pp4 != 0 {
|
||||||
|
for _, a := range p.Addresses().All() {
|
||||||
|
if a.IsSingleIP() && a.Addr().Is4() {
|
||||||
|
peerURL = fmt.Sprintf("http://%v", netip.AddrPortFrom(a.Addr(), pp4))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if peerURL == "" && selfHave6 && pp6 != 0 {
|
||||||
|
for _, a := range p.Addresses().All() {
|
||||||
|
if a.IsSingleIP() && a.Addr().Is6() {
|
||||||
|
peerURL = fmt.Sprintf("http://%v", netip.AddrPortFrom(a.Addr(), pp6))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return jsNetMapPeerNode{
|
return jsNetMapPeerNode{
|
||||||
jsNetMapNode: jsNetMapNode{
|
jsNetMapNode: jsNetMapNode{
|
||||||
@@ -516,7 +516,6 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
|||||||
MachineKey: p.Machine().String(),
|
MachineKey: p.Machine().String(),
|
||||||
NodeKey: p.Key().String(),
|
NodeKey: p.Key().String(),
|
||||||
PeerAPIURL: peerURL,
|
PeerAPIURL: peerURL,
|
||||||
Services: userServicesFromView(p.Hostinfo().Services()),
|
|
||||||
},
|
},
|
||||||
Online: p.Online().Clone(),
|
Online: p.Online().Clone(),
|
||||||
TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
|
TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
|
||||||
@@ -595,17 +594,14 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
ln, err := safesocket.Listen("")
|
ln, err := safesocket.Listen("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("safesocket.Listen: %v", err)
|
log.Fatalf("safesocket.Listen: %v", err)
|
||||||
}
|
}
|
||||||
i.ln = ln
|
|
||||||
|
|
||||||
go func() {
|
err = i.srv.Run(context.Background(), ln)
|
||||||
err := i.srv.Run(context.Background(), ln)
|
|
||||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
|
||||||
log.Fatalf("ipnserver.Run exited: %v", err)
|
log.Fatalf("ipnserver.Run exited: %v", err)
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,21 +620,6 @@ func (i *jsIPN) logout() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *jsIPN) shutdown() js.Value {
|
|
||||||
return makePromise(func() (any, error) {
|
|
||||||
i.shutdownOnce.Do(func() {
|
|
||||||
if i.lb != nil {
|
|
||||||
i.lb.Shutdown()
|
|
||||||
}
|
|
||||||
if i.ln != nil {
|
|
||||||
i.ln.Close()
|
|
||||||
}
|
|
||||||
close(i.shutdownCh)
|
|
||||||
})
|
|
||||||
return nil, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
|
func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
|
||||||
jsSSHSession := &jsSSHSession{
|
jsSSHSession := &jsSSHSession{
|
||||||
jsIPN: i,
|
jsIPN: i,
|
||||||
@@ -1352,39 +1333,6 @@ func (i *jsIPN) suggestExitNode() js.Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *jsIPN) setServices(jsServices js.Value) js.Value {
|
|
||||||
return makePromise(func() (any, error) {
|
|
||||||
n := jsServices.Length()
|
|
||||||
svcs := make([]tailcfg.Service, 0, n)
|
|
||||||
for idx := range n {
|
|
||||||
s := jsServices.Index(idx)
|
|
||||||
proto := tailcfg.ServiceProto(s.Get("proto").String())
|
|
||||||
port := uint16(s.Get("port").Int())
|
|
||||||
var desc string
|
|
||||||
if d := s.Get("description"); d.Type() == js.TypeString {
|
|
||||||
desc = d.String()
|
|
||||||
}
|
|
||||||
svcs = append(svcs, tailcfg.Service{Proto: proto, Port: port, Description: desc})
|
|
||||||
}
|
|
||||||
i.lb.SetExplicitServices(svcs)
|
|
||||||
return nil, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// userServicesFromView converts a hostinfo services slice to jsService entries,
|
|
||||||
// filtering out internal peerapi protocol entries (already reflected in peerAPIURL).
|
|
||||||
func userServicesFromView(svcs views.Slice[tailcfg.Service]) []jsService {
|
|
||||||
out := make([]jsService, 0, svcs.Len())
|
|
||||||
for _, s := range svcs.All() {
|
|
||||||
switch s.Proto {
|
|
||||||
case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, jsService{Proto: string(s.Proto), Port: s.Port, Description: s.Description})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *jsIPN) localAPI(method, path, body string) js.Value {
|
func (i *jsIPN) localAPI(method, path, body string) js.Value {
|
||||||
return makePromise(func() (any, error) {
|
return makePromise(func() (any, error) {
|
||||||
h := localapi.NewHandler(localapi.HandlerConfig{
|
h := localapi.NewHandler(localapi.HandlerConfig{
|
||||||
@@ -1621,19 +1569,12 @@ type jsNetMap struct {
|
|||||||
LockedOut bool `json:"lockedOut"`
|
LockedOut bool `json:"lockedOut"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsService struct {
|
|
||||||
Proto string `json:"proto"`
|
|
||||||
Port uint16 `json:"port"`
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type jsNetMapNode struct {
|
type jsNetMapNode struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Addresses []string `json:"addresses"`
|
Addresses []string `json:"addresses"`
|
||||||
MachineKey string `json:"machineKey"`
|
MachineKey string `json:"machineKey"`
|
||||||
NodeKey string `json:"nodeKey"`
|
NodeKey string `json:"nodeKey"`
|
||||||
PeerAPIURL string `json:"peerAPIURL,omitempty"`
|
PeerAPIURL string `json:"peerAPIURL,omitempty"`
|
||||||
Services []jsService `json:"services"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsNetMapSelfNode struct {
|
type jsNetMapSelfNode struct {
|
||||||
|
|||||||
@@ -302,12 +302,6 @@ func (c *Auto) restartMap() {
|
|||||||
c.updateControl()
|
c.updateControl()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestartMap cancels the existing map poll and starts a fresh streaming one,
|
|
||||||
// forcing the control server to send a new full netmap response.
|
|
||||||
func (c *Auto) RestartMap() {
|
|
||||||
c.restartMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Auto) authRoutine() {
|
func (c *Auto) authRoutine() {
|
||||||
defer close(c.authDone)
|
defer close(c.authDone)
|
||||||
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
|
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
|
||||||
|
|||||||
@@ -294,7 +294,6 @@ type LocalBackend struct {
|
|||||||
capTailnetLock bool // whether netMap contains the tailnet lock capability
|
capTailnetLock bool // whether netMap contains the tailnet lock capability
|
||||||
// hostinfo is mutated in-place while mu is held.
|
// hostinfo is mutated in-place while mu is held.
|
||||||
hostinfo *tailcfg.Hostinfo // TODO(nickkhyl): move to nodeBackend
|
hostinfo *tailcfg.Hostinfo // TODO(nickkhyl): move to nodeBackend
|
||||||
explicitServices []tailcfg.Service // services set explicitly via SetExplicitServices; always uploaded
|
|
||||||
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil; TODO(nickkhyl): move to nodeBackend
|
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil; TODO(nickkhyl): move to nodeBackend
|
||||||
activeLogin string // last logged LoginName from netMap; TODO(nickkhyl): move to nodeBackend (or remove? it's in [ipn.LoginProfile]).
|
activeLogin string // last logged LoginName from netMap; TODO(nickkhyl): move to nodeBackend (or remove? it's in [ipn.LoginProfile]).
|
||||||
engineStatus ipn.EngineStatus
|
engineStatus ipn.EngineStatus
|
||||||
@@ -4968,30 +4967,6 @@ func (b *LocalBackend) setPortlistServices(sl []tailcfg.Service) {
|
|||||||
b.doSetHostinfoFilterServices()
|
b.doSetHostinfoFilterServices()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetExplicitServices sets the services this node advertises on the netmap.
|
|
||||||
// Unlike the OS port-scan path (setPortlistServices), services set here are
|
|
||||||
// always uploaded to the control server regardless of the ShouldUploadServices
|
|
||||||
// hook — suitable for environments like browser WASM where OS port scanning is
|
|
||||||
// unavailable and services are declared programmatically.
|
|
||||||
func (b *LocalBackend) SetExplicitServices(sl []tailcfg.Service) {
|
|
||||||
b.mu.Lock()
|
|
||||||
if b.hostinfo == nil {
|
|
||||||
b.hostinfo = new(tailcfg.Hostinfo)
|
|
||||||
}
|
|
||||||
b.hostinfo.Services = sl
|
|
||||||
b.explicitServices = sl
|
|
||||||
ccAuto := b.ccAuto
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
b.doSetHostinfoFilterServices()
|
|
||||||
// Restart the streaming map poll so the control server sends back a fresh
|
|
||||||
// netmap that includes our updated services in SelfNode, and so peers
|
|
||||||
// receive the update promptly via the control server's push.
|
|
||||||
if ccAuto != nil {
|
|
||||||
ccAuto.RestartMap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
|
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
|
||||||
// possibly after mangling the given hostinfo.
|
// possibly after mangling the given hostinfo.
|
||||||
//
|
//
|
||||||
@@ -5036,10 +5011,8 @@ func (b *LocalBackend) hostInfoWithServicesLocked() *tailcfg.Hostinfo {
|
|||||||
// Make a shallow copy of hostinfo so we can mutate
|
// Make a shallow copy of hostinfo so we can mutate
|
||||||
// at the Service field.
|
// at the Service field.
|
||||||
if f, ok := b.extHost.Hooks().ShouldUploadServices.GetOk(); !ok || !f() {
|
if f, ok := b.extHost.Hooks().ShouldUploadServices.GetOk(); !ok || !f() {
|
||||||
if len(b.explicitServices) == 0 {
|
|
||||||
hi.Services = []tailcfg.Service{}
|
hi.Services = []tailcfg.Service{}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Don't mutate hi.Service's underlying array. Append to
|
// Don't mutate hi.Service's underlying array. Append to
|
||||||
// the slice with no free capacity.
|
// the slice with no free capacity.
|
||||||
|
|||||||
Reference in New Issue
Block a user