mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-31 05:23:14 +00:00
Merge 9e5e4de2bcdaa1877606a471217374119a54f3db into b3455fa99a5e8d07133d5140017ec7c49f032a07
This commit is contained in:
commit
73f77e7776
@ -160,6 +160,7 @@ type serveEnv struct {
|
|||||||
http uint // HTTP port
|
http uint // HTTP port
|
||||||
tcp uint // TCP port
|
tcp uint // TCP port
|
||||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||||
|
promoteHTTPS bool // promote HTTP to HTTPS
|
||||||
subcmd serveMode // subcommand
|
subcmd serveMode // subcommand
|
||||||
yes bool // update without prompt
|
yes bool // update without prompt
|
||||||
|
|
||||||
|
@ -124,6 +124,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
|||||||
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
||||||
if subcmd == serve {
|
if subcmd == serve {
|
||||||
fs.UintVar(&e.http, "http", 0, "Expose an HTTP server at the specified port")
|
fs.UintVar(&e.http, "http", 0, "Expose an HTTP server at the specified port")
|
||||||
|
fs.BoolVar(&e.promoteHTTPS, "promote-https", false, "Promote HTTP to HTTPS (default false)")
|
||||||
}
|
}
|
||||||
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||||
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||||
@ -224,6 +225,11 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
return errHelpFunc(subcmd)
|
return errHelpFunc(subcmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if srvType == serveTypeHTTP && e.promoteHTTPS {
|
||||||
|
fmt.Fprintf(e.stderr(), "error: --promote-https is only valid for HTTPS\n\n")
|
||||||
|
return errHelpFunc(subcmd)
|
||||||
|
}
|
||||||
|
|
||||||
sc, err := e.lc.GetServeConfig(ctx)
|
sc, err := e.lc.GetServeConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting serve config: %w", err)
|
return fmt.Errorf("error getting serve config: %w", err)
|
||||||
@ -296,7 +302,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
|||||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
|
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel, e.promoteHTTPS)
|
||||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -366,7 +372,7 @@ func serveFromPortHandler(tcp *ipn.TCPPortHandler) serveType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
|
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, promoteHTTPS bool) error {
|
||||||
// update serve config based on the type
|
// update serve config based on the type
|
||||||
switch srvType {
|
switch srvType {
|
||||||
case serveTypeHTTPS, serveTypeHTTP:
|
case serveTypeHTTPS, serveTypeHTTP:
|
||||||
@ -391,6 +397,8 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
|||||||
// update the serve config based on if funnel is enabled
|
// update the serve config based on if funnel is enabled
|
||||||
e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
|
e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
|
||||||
|
|
||||||
|
sc.SetRedirectToHTTPS(dnsName, srvPort, promoteHTTPS)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,9 +232,10 @@ func (src *HTTPHandler) Clone() *HTTPHandler {
|
|||||||
|
|
||||||
// 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 _HTTPHandlerCloneNeedsRegeneration = HTTPHandler(struct {
|
var _HTTPHandlerCloneNeedsRegeneration = HTTPHandler(struct {
|
||||||
Path string
|
Path string
|
||||||
Proxy string
|
Proxy string
|
||||||
Text string
|
Text string
|
||||||
|
Redirect string
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// Clone makes a deep copy of WebServerConfig.
|
// Clone makes a deep copy of WebServerConfig.
|
||||||
|
@ -456,15 +456,17 @@ func (v *HTTPHandlerView) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v HTTPHandlerView) Path() string { return v.ж.Path }
|
func (v HTTPHandlerView) Path() string { return v.ж.Path }
|
||||||
func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy }
|
func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy }
|
||||||
func (v HTTPHandlerView) Text() string { return v.ж.Text }
|
func (v HTTPHandlerView) Text() string { return v.ж.Text }
|
||||||
|
func (v HTTPHandlerView) Redirect() string { return v.ж.Redirect }
|
||||||
|
|
||||||
// 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 _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct {
|
var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct {
|
||||||
Path string
|
Path string
|
||||||
Proxy string
|
Proxy string
|
||||||
Text string
|
Text string
|
||||||
|
Redirect string
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// View returns a read-only view of WebServerConfig.
|
// View returns a read-only view of WebServerConfig.
|
||||||
|
@ -374,9 +374,9 @@ type LocalBackend struct {
|
|||||||
webClient webClient
|
webClient webClient
|
||||||
webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic
|
webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic
|
||||||
|
|
||||||
serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic
|
serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic
|
||||||
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy
|
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy
|
||||||
|
serveRedirectHandlers sync.Map // string (HTTPHandler.Redirect) => http.Handler
|
||||||
// statusLock must be held before calling statusChanged.Wait() or
|
// statusLock must be held before calling statusChanged.Wait() or
|
||||||
// statusChanged.Broadcast().
|
// statusChanged.Broadcast().
|
||||||
statusLock sync.Mutex
|
statusLock sync.Mutex
|
||||||
@ -6531,6 +6531,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.setServeProxyHandlersLocked()
|
b.setServeProxyHandlersLocked()
|
||||||
|
b.setServeRedirectHandlersLocked()
|
||||||
|
|
||||||
// don't listen on netmap addresses if we're in userspace mode
|
// don't listen on netmap addresses if we're in userspace mode
|
||||||
if !b.sys.IsNetstack() {
|
if !b.sys.IsNetstack() {
|
||||||
@ -6614,6 +6615,51 @@ func (b *LocalBackend) setServeProxyHandlersLocked() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setServeRedirectHandlersLocked ensures there is an http redirect handler for
|
||||||
|
// each redirect specified in serveConfig. It expects serveConfig to be valid
|
||||||
|
// and up-to-date, so should be called after reloadServeConfigLocked.
|
||||||
|
func (b *LocalBackend) setServeRedirectHandlersLocked() {
|
||||||
|
if !b.serveConfig.Valid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var redirects map[string]bool
|
||||||
|
b.serveConfig.RangeOverWebs(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) {
|
||||||
|
conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) {
|
||||||
|
redirect := h.Redirect()
|
||||||
|
if redirect == "" {
|
||||||
|
// Only create redirect handlers for servers with a redirect target.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
mak.Set(&redirects, redirect, true)
|
||||||
|
if _, ok := b.serveRedirectHandlers.Load(redirect); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logf("serve: creating a new redirect handler for %s", redirect)
|
||||||
|
rh, err := b.redirectHandlerForRedirect(redirect, 301)
|
||||||
|
if err != nil {
|
||||||
|
b.logf("[unexpected] could not create redirect handler for %v: %s", redirect, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
b.serveRedirectHandlers.Store(redirect, rh)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clean up redirect handlers that are no longer present in configuration.
|
||||||
|
b.serveRedirectHandlers.Range(func(key, value any) bool {
|
||||||
|
redirect := key.(string)
|
||||||
|
if !redirects[redirect] {
|
||||||
|
b.logf("serve: closing idle connections to %s", redirect)
|
||||||
|
b.serveRedirectHandlers.Delete(redirect)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
@ -804,6 +804,21 @@ func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
|
|||||||
return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType)
|
return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redirectHandlerForRedirect creates a new HTTP redirect handler for a particular url.
|
||||||
|
// `targetURL` is a HTTPHandler.Redirect string (url).
|
||||||
|
func (b *LocalBackend) redirectHandlerForRedirect(targetURL string, code int) (http.Handler, error) {
|
||||||
|
u, err := url.Parse(targetURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u.Path = r.URL.Path
|
||||||
|
u.RawPath = r.URL.RawPath
|
||||||
|
http.Redirect(w, r, u.String(), code)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// isGRPC accepts an HTTP request's content type header value and determines
|
// isGRPC accepts an HTTP request's content type header value and determines
|
||||||
// whether this is gRPC content. grpc-go considers a value that equals
|
// whether this is gRPC content. grpc-go considers a value that equals
|
||||||
// application/grpc or has a prefix of application/grpc+ or application/grpc; a
|
// application/grpc or has a prefix of application/grpc+ or application/grpc; a
|
||||||
@ -901,6 +916,15 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if v := h.Redirect(); v != "" {
|
||||||
|
h, ok := b.serveRedirectHandlers.Load(v)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "unknown redirect destination", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.(http.Handler).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
http.Error(w, "empty handler", 500)
|
http.Error(w, "empty handler", 500)
|
||||||
}
|
}
|
||||||
|
@ -658,6 +658,63 @@ func TestServeHTTPProxyPath(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeHTTPRedirect(t *testing.T) {
|
||||||
|
b := newTestBackend(t)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
requestPath string
|
||||||
|
wantPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "http / -> https /",
|
||||||
|
requestPath: "/",
|
||||||
|
wantPath: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "http /foo -> https /foo",
|
||||||
|
requestPath: "/foo",
|
||||||
|
wantPath: "/foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
conf := &ipn.ServeConfig{
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"example.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Redirect: "https://example.ts.net"},
|
||||||
|
}},
|
||||||
|
"example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Text: "foo bar"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := b.SetServeConfig(conf, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req := &http.Request{
|
||||||
|
URL: &url.URL{Path: tt.requestPath},
|
||||||
|
TLS: &tls.ConnectionState{ServerName: "example.ts.net"},
|
||||||
|
}
|
||||||
|
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(),
|
||||||
|
&serveHTTPContext{
|
||||||
|
DestPort: 80,
|
||||||
|
SrcAddr: netip.MustParseAddrPort("1.2.3.4:1234"), // random src
|
||||||
|
}))
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
b.serveWebHandler(w, req)
|
||||||
|
|
||||||
|
// Verify what path was requested
|
||||||
|
p := w.Result().Header.Get("Location")
|
||||||
|
wantLoc := fmt.Sprintf("https://example.ts.net%s", tt.wantPath)
|
||||||
|
if p != wantLoc {
|
||||||
|
t.Errorf("wanted request path %s got %s", wantLoc, p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeHTTPProxyHeaders(t *testing.T) {
|
func TestServeHTTPProxyHeaders(t *testing.T) {
|
||||||
b := newTestBackend(t)
|
b := newTestBackend(t)
|
||||||
|
|
||||||
|
39
ipn/serve.go
39
ipn/serve.go
@ -160,6 +160,8 @@ type HTTPHandler struct {
|
|||||||
|
|
||||||
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
|
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
|
||||||
|
|
||||||
|
Redirect string `json:",omitempty"` // redirect requests to this URL
|
||||||
|
|
||||||
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
|
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
|
||||||
// temporary ones? Error codes? Redirects?
|
// temporary ones? Error codes? Redirects?
|
||||||
}
|
}
|
||||||
@ -344,6 +346,43 @@ func (sc *ServeConfig) SetFunnel(host string, port uint16, setOn bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRedirectToHTTPS configures a TCPPortHandler and HTTPHandler to redirect all
|
||||||
|
// traffic from port 80 to HTTPS, using the provided `host` and `port` for the
|
||||||
|
// redirect URL. If setOn is false, it removes any active redirect handlers on
|
||||||
|
// port 80.
|
||||||
|
func (sc *ServeConfig) SetRedirectToHTTPS(host string, port uint16, setOn bool) {
|
||||||
|
if sc == nil {
|
||||||
|
sc = new(ServeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
hp := HostPort(net.JoinHostPort(host, "80"))
|
||||||
|
if setOn {
|
||||||
|
mak.Set(&sc.TCP, 80, &TCPPortHandler{HTTP: true})
|
||||||
|
var url string
|
||||||
|
if port == 443 {
|
||||||
|
url = fmt.Sprintf("https://%s", host)
|
||||||
|
} else {
|
||||||
|
url = fmt.Sprintf("https://%s:%d", host, port)
|
||||||
|
}
|
||||||
|
handler := &HTTPHandler{Redirect: url}
|
||||||
|
if _, ok := sc.Web[hp]; !ok {
|
||||||
|
mak.Set(&sc.Web, hp, new(WebServerConfig))
|
||||||
|
}
|
||||||
|
// Overwrite any existing handlers as we're handling all HTTP traffic.
|
||||||
|
sc.Web[hp].Handlers = map[string]*HTTPHandler{"/": handler}
|
||||||
|
} else {
|
||||||
|
// If we're running with HTTP to HTTPS promotion, we need to remove any
|
||||||
|
// existing Redirect handlers.
|
||||||
|
if tcph, exists := sc.TCP[80]; exists && tcph.HTTP {
|
||||||
|
if wh, exists := sc.Web[hp]; exists {
|
||||||
|
if wh.Handlers["/"].Redirect != "" {
|
||||||
|
delete(wh.Handlers, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveWebHandler deletes the web handlers at all of the given mount points
|
// RemoveWebHandler deletes the web handlers at all of the given mount points
|
||||||
// for the provided host and port in the serve config. If cleanupFunnel is
|
// for the provided host and port in the serve config. If cleanupFunnel is
|
||||||
// true, this also removes the funnel value for this port if no handlers remain.
|
// true, this also removes the funnel value for this port if no handlers remain.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user