{"record":{"id":"97f4ef76b935122e","repo":"tailscale/tailscale","slug":"listenfunnel-q-q-host-must-be-empty","errorCode":null,"errorMessage":"ListenFunnel(%q, %q): host must be empty","messagePattern":"ListenFunnel\\(%q, %q\\): host must be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tsnet/tsnet.go","lineNumber":1521,"sourceCode":"// Currently (2023-03-10), Funnel only supports TCP on ports 443, 8443, and 10000.\n// The supported host name is limited to that configured for the tsnet.Server.\n// As such, the standard way to create funnel is:\n//\n//\ts.ListenFunnel(\"tcp\", \":443\")\n//\n// and the only other supported addrs currently are \":8443\" and \":10000\".\n//\n// It will start the server if it has not been started yet.\nfunc (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.Listener, error) {\n\tif network != \"tcp\" {\n\t\treturn nil, fmt.Errorf(\"ListenFunnel(%q, %q): only tcp is supported\", network, addr)\n\t}\n\thost, portStr, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif host != \"\" {\n\t\treturn nil, fmt.Errorf(\"ListenFunnel(%q, %q): host must be empty\", network, addr)\n\t}\n\tport, err := strconv.ParseUint(portStr, 10, 16)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Process, validate opts.\n\tlnOn := listenOnBoth\n\tvar tlsConfig *tls.Config\n\tfor _, opt := range opts {\n\t\tswitch v := opt.(type) {\n\t\tcase funnelTLSConfig:\n\t\t\tif v.conf == nil {\n\t\t\t\treturn nil, errors.New(\"invalid nil FunnelTLSConfig\")\n\t\t\t}\n\t\t\ttlsConfig = v.conf\n\t\tcase funnelOnly:\n\t\t\tlnOn = listenOnFunnel","sourceCodeStart":1503,"sourceCodeEnd":1539,"githubUrl":"https://github.com/tailscale/tailscale/blob/57c3357fdb542d26c6f9e9f0b815ae5077e63d77/tsnet/tsnet.go#L1503-L1539","documentation":"Funnel listeners cannot pin a specific local address because Funnel traffic arrives for the node's certificate domain, not an interface IP. After SplitHostPort, tsnet requires the host part of addr to be empty — i.e. exactly \":443\", \":8443\", or \":10000\". This error fires when a host was supplied (the network check and SplitHostPort already succeeded).","triggerScenarios":"Calling s.ListenFunnel(\"tcp\", \"0.0.0.0:443\"), (\"tcp\", \"localhost:443\"), or (\"tcp\", \"100.101.102.103:443\") — any addr with a non-empty host component.","commonSituations":"Reusing a listen address string from a standard net.Listen config (commonly \"0.0.0.0:443\") ; templates that always join a host; copying the Funnel example but adding the node's IP.","solutions":["Drop the host: s.ListenFunnel(\"tcp\", \":443\").","Sanitize configured addresses by stripping the host before calling ListenFunnel (keep only the port).","Remember the hostname is fixed to the node's cert domain anyway, so a host adds nothing."],"exampleFix":"// before\nln, err := s.ListenFunnel(\"tcp\", \"0.0.0.0:443\")\n\n// after\nln, err := s.ListenFunnel(\"tcp\", \":443\")","handlingStrategy":"validation","validationCode":"if host, _, err := net.SplitHostPort(addr); err != nil || host != \"\" {\n    return nil, fmt.Errorf(\"ListenFunnel addr must be \\\":port\\\" (got %q)\", addr)\n}","typeGuard":"func isFunnelAddrValid(addr string) bool {\n    host, _, err := net.SplitHostPort(addr)\n    return err == nil && host == \"\"\n}","tryCatchPattern":null,"preventionTips":["Strip the host from operator-provided address strings before ListenFunnel (keep ':port').","Lint configs for funnel addresses not matching '^:(443|8443|10000)$'."],"tags":["go","tailscale","tsnet","funnel","listen-address","api-misuse"],"backgroundTag":"invalid-listen-address","analyzedSha":"57c3357fdb542d26c6f9e9f0b815ae5077e63d77","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}