{"record":{"id":"b0a87e55134e6fbd","repo":"fatedier/frp","slug":"xtcp-connection-of-s-auth-failed","errorCode":null,"errorMessage":"xtcp connection of [%s] auth failed","messagePattern":"xtcp connection of \\[(.+?)\\] auth failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/nathole/controller.go","lineNumber":190,"sourceCode":"\t\tsid:                sid,\n\t\tvisitorMsg:         m,\n\t\tvisitorTransporter: transporter,\n\t\tnotifyCh:           make(chan struct{}, 1),\n\t}\n\tvar (\n\t\tclientCfg *ClientCfg\n\t\tok        bool\n\t)\n\terr := func() error {\n\t\tc.mu.Lock()\n\t\tdefer c.mu.Unlock()\n\n\t\tclientCfg, ok = c.clientCfgs[m.ProxyName]\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"xtcp server for [%s] doesn't exist\", m.ProxyName)\n\t\t}\n\t\tif !util.ConstantTimeEqString(m.SignKey, util.GetAuthKey(clientCfg.sk, m.Timestamp)) {\n\t\t\treturn fmt.Errorf(\"xtcp connection of [%s] auth failed\", m.ProxyName)\n\t\t}\n\t\tc.sessions[sid] = session\n\t\treturn nil\n\t}()\n\tif err != nil {\n\t\tlog.Warnf(\"handle visitorMsg error: %v\", err)\n\t\t_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, err.Error()))\n\t\treturn\n\t}\n\tlog.Tracef(\"handle visitor message, sid [%s], server name: %s\", sid, m.ProxyName)\n\n\tdefer func() {\n\t\tc.mu.Lock()\n\t\tdefer c.mu.Unlock()\n\t\tdelete(c.sessions, sid)\n\t}()\n\n\tif err := errors.PanicToError(func() {","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/nathole/controller.go#L172-L208","documentation":"Thrown in the nathole Controller's visitor-message handler (pkg/nathole/controller.go) when the visitor's SignKey does not match the expected auth key derived from the client's secret key and timestamp (util.GetAuthKey(clientCfg.sk, m.Timestamp), compared in constant time). The xtcp visitor must share the same secret key (sk) as the xtcp proxy's client; otherwise authentication fails and the error is returned to the visitor in the NatHoleResp.","triggerScenarios":"A NatHoleVisitor message whose ProxyName resolves to a registered client, but whose sk differs from the client's configured secret. Common with copy-pasted or rotated sk values, or when the visitor config omits sk entirely (falling back to a different default).","commonSituations":"sk in the visitor block does not match sk on the client's xtcp proxy; the secret was rotated on one side only; sk omitted from the visitor config so an empty/default value is signed with.","solutions":["Set the same sk value in the visitor config as in the client's xtcp proxy config","After rotating secrets, update and restart both client and visitor","Check for trailing whitespace/newlines when pasting sk into config files","Verify the timestamp is not wildly off (clock skew affects key derivation inputs)"],"exampleFix":"# before (visitor)\n[[visitors]]\nname = \"p2p-v\"\ntype = \"xtcp\"\nserverName = \"p2p\"\nsk = \"old-secret\"\n\n# after\n[[visitors]]\nname = \"p2p-v\"\ntype = \"xtcp\"\nserverName = \"p2p\"\nsk = \"same-secret-as-client\"","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := nathole.PreCheck(ctx, tp, proxyName, timeout); err != nil {\n    if strings.Contains(err.Error(), \"auth failed\") {\n        // fix sk in visitor config to match client, then retry\n    }\n}","preventionTips":["Keep sk identical on client xtcp proxy and visitor","Rotate secrets on both sides together","Watch for whitespace when pasting sk values"],"tags":["go","frp","nathole","xtcp","authentication","configuration"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}