diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index c00051da4..f5d1f0410 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -82,9 +82,9 @@ type Direct struct { dialPlan ControlDialPlanner // can be nil - mu sync.Mutex // mutex guards the following fields - serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key - serverNoiseKey key.MachinePublic + mu sync.Mutex // mutex guards the following fields + serverLegacyKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key; only used for signRegisterRequest on Windows now + serverNoiseKey key.MachinePublic sfGroup singleflight.Group[struct{}, *NoiseClient] // protects noiseClient creation. noiseClient *NoiseClient @@ -436,12 +436,6 @@ type loginOpt struct { OldNodeKeySignature tkatype.MarshaledSignature } -// httpClient provides a common interface for the noiseClient and -// the NaCl box http.Client. -type httpClient interface { - Do(req *http.Request) (*http.Response, error) -} - // hostInfoLocked returns a Clone of c.hostinfo and c.netinfo. // It must only be called with c.mu held. func (c *Direct) hostInfoLocked() *tailcfg.Hostinfo { @@ -454,7 +448,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new c.mu.Lock() persist := c.persist.AsStruct() tryingNewKey := c.tryingNewKey - serverKey := c.serverKey + serverKey := c.serverLegacyKey serverNoiseKey := c.serverNoiseKey authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf) hi := c.hostInfoLocked() @@ -494,20 +488,22 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString()) c.mu.Lock() - c.serverKey = keys.LegacyPublicKey + c.serverLegacyKey = keys.LegacyPublicKey c.serverNoiseKey = keys.PublicKey c.mu.Unlock() serverKey = keys.LegacyPublicKey serverNoiseKey = keys.PublicKey - // For servers supporting the Noise transport, - // proactively shut down our TLS TCP connection. + // Proactively shut down our TLS TCP connection. // We're not going to need it and it's nicer to the // server. - if !serverNoiseKey.IsZero() { - c.httpc.CloseIdleConnections() - } + c.httpc.CloseIdleConnections() + } + + if serverNoiseKey.IsZero() { + return false, "", nil, errors.New("control server is too old; no noise key") } + var oldNodeKey key.NodePublic switch { case opt.Logout: @@ -594,7 +590,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new request.Auth.Provider = persist.Provider request.Auth.LoginName = persist.UserProfile.LoginName request.Auth.AuthKey = authKey - err = signRegisterRequest(&request, c.serverURL, c.serverKey, machinePrivKey.Public()) + err = signRegisterRequest(&request, c.serverURL, c.serverLegacyKey, machinePrivKey.Public()) if err != nil { // If signing failed, clear all related fields request.SignatureType = tailcfg.SignatureNone @@ -614,21 +610,16 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new } // URL and httpc are protocol specific. - var url string - var httpc httpClient - if serverNoiseKey.IsZero() { - httpc = c.httpc - url = fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().UntypedHexString()) - } else { - request.Version = tailcfg.CurrentCapabilityVersion - httpc, err = c.getNoiseClient() - if err != nil { - return regen, opt.URL, nil, fmt.Errorf("getNoiseClient: %w", err) - } - url = fmt.Sprintf("%s/machine/register", c.serverURL) - url = strings.Replace(url, "http:", "https:", 1) + + request.Version = tailcfg.CurrentCapabilityVersion + httpc, err := c.getNoiseClient() + if err != nil { + return regen, opt.URL, nil, fmt.Errorf("getNoiseClient: %w", err) } - bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey) + url := fmt.Sprintf("%s/machine/register", c.serverURL) + url = strings.Replace(url, "http:", "https:", 1) + + bodyData, err := encode(request) if err != nil { return regen, opt.URL, nil, err } @@ -650,7 +641,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new res.StatusCode, strings.TrimSpace(string(msg))) } resp := tailcfg.RegisterResponse{} - if err := decode(res, &resp, serverKey, serverNoiseKey, machinePrivKey); err != nil { + if err := decode(res, &resp); err != nil { c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err) return regen, opt.URL, nil, fmt.Errorf("register request: %v", err) } @@ -844,7 +835,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap c.mu.Lock() persist := c.persist serverURL := c.serverURL - serverKey := c.serverKey serverNoiseKey := c.serverNoiseKey hi := c.hostInfoLocked() backendLogID := hi.BackendLogID @@ -858,6 +848,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap } c.mu.Unlock() + if serverNoiseKey.IsZero() { + return errors.New("control server is too old; no noise key") + } + machinePrivKey, err := c.getMachinePrivKey() if err != nil { return fmt.Errorf("getMachinePrivKey: %w", err) @@ -914,7 +908,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap } request.Compress = "zstd" - bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey) + bodyData, err := encode(request) if err != nil { vlogf("netmap: encode: %v", err) return err @@ -926,20 +920,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap machinePubKey := machinePrivKey.Public() t0 := c.clock.Now() - // Url and httpc are protocol specific. - var url string - var httpc httpClient - if serverNoiseKey.IsZero() { - httpc = c.httpc - url = fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.UntypedHexString()) - } else { - httpc, err = c.getNoiseClient() - if err != nil { - return fmt.Errorf("getNoiseClient: %w", err) - } - url = fmt.Sprintf("%s/machine/map", serverURL) - url = strings.Replace(url, "http:", "https:", 1) + httpc, err := c.getNoiseClient() + if err != nil { + return fmt.Errorf("getNoiseClient: %w", err) } + url := fmt.Sprintf("%s/machine/map", serverURL) + url = strings.Replace(url, "http:", "https:", 1) // Create a watchdog timer that breaks the connection if we don't receive a // MapResponse from the network at least once every two minutes. The @@ -1047,7 +1033,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond)) var resp tailcfg.MapResponse - if err := c.decodeMsg(msg, &resp, machinePrivKey); err != nil { + if err := c.decodeMsg(msg, &resp); err != nil { vlogf("netmap: decode error: %v", err) return err } @@ -1164,9 +1150,8 @@ func initDisplayNames(selfNode tailcfg.NodeView, resp *tailcfg.MapResponse) { } } -// decode JSON decodes the res.Body into v. If serverNoiseKey is not specified, -// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box. -func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error { +// decode JSON decodes the res.Body into v. +func decode(res *http.Response, v any) error { defer res.Body.Close() msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20)) if err != nil { @@ -1175,10 +1160,7 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl if res.StatusCode != 200 { return fmt.Errorf("%d: %v", res.StatusCode, string(msg)) } - if !serverNoiseKey.IsZero() { - return json.Unmarshal(msg, v) - } - return decodeMsg(msg, v, serverKey, mkey) + return json.Unmarshal(msg, v) } var ( @@ -1189,25 +1171,8 @@ var ( var jsonEscapedZero = []byte(`\u0000`) // decodeMsg is responsible for uncompressing msg and unmarshaling into v. -// If c.serverNoiseKey is not specified, it uses the c.serverKey and mkey -// to first the decrypt msg from the NaCl-crypto-box. -func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error { - c.mu.Lock() - serverKey := c.serverKey - serverNoiseKey := c.serverNoiseKey - c.mu.Unlock() - - var decrypted []byte - if serverNoiseKey.IsZero() { - var ok bool - decrypted, ok = mkey.OpenFrom(serverKey, msg) - if !ok { - return errors.New("cannot decrypt response") - } - } else { - decrypted = msg - } - b, err := zstdframe.AppendDecode(nil, decrypted) +func (c *Direct) decodeMsg(compressedMsg []byte, v any) error { + b, err := zstdframe.AppendDecode(nil, compressedMsg) if err != nil { return err } @@ -1224,26 +1189,11 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error { return fmt.Errorf("response: %v", err) } return nil - -} - -func decodeMsg(msg []byte, v any, serverKey key.MachinePublic, machinePrivKey key.MachinePrivate) error { - decrypted, ok := machinePrivKey.OpenFrom(serverKey, msg) - if !ok { - return errors.New("cannot decrypt response") - } - if bytes.Contains(decrypted, jsonEscapedZero) { - log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted) - } - if err := json.Unmarshal(decrypted, v); err != nil { - return fmt.Errorf("response: %v", err) - } - return nil } -// encode JSON encodes v. If serverNoiseKey is not specified, it uses the serverKey and mkey to -// seal the message into a NaCl-crypto-box. -func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) ([]byte, error) { +// encode JSON encodes v as JSON, logging tailcfg.MapRequest values if +// debugMap is set. +func encode(v any) ([]byte, error) { b, err := json.Marshal(v) if err != nil { return nil, err @@ -1253,10 +1203,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine log.Printf("MapRequest: %s", b) } } - if !serverNoiseKey.IsZero() { - return b, nil - } - return mkey.SealTo(serverKey, b), nil + return b, nil } func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string) (*tailcfg.OverTLSPublicKeyResponse, error) { @@ -1353,7 +1300,7 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool { func (c *Direct) answerPing(pr *tailcfg.PingRequest) { httpc := c.httpc - useNoise := pr.URLIsNoise || pr.Types == "c2n" && c.noiseConfigured() + useNoise := pr.URLIsNoise || pr.Types == "c2n" if useNoise { nc, err := c.getNoiseClient() if err != nil { @@ -1554,14 +1501,6 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er return nil } -// noiseConfigured reports whether the client can communicate with Control -// over Noise. -func (c *Direct) noiseConfigured() bool { - c.mu.Lock() - defer c.mu.Unlock() - return !c.serverNoiseKey.IsZero() -} - // SetDNS sends the SetDNSRequest request to the control plane server, // requesting a DNS record be created or updated. func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) { @@ -1571,53 +1510,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er metricSetDNSError.Add(1) } }() - if c.noiseConfigured() { - return c.setDNSNoise(ctx, req) - } - c.mu.Lock() - serverKey := c.serverKey - c.mu.Unlock() - - if serverKey.IsZero() { - return errors.New("zero serverKey") - } - machinePrivKey, err := c.getMachinePrivKey() - if err != nil { - return fmt.Errorf("getMachinePrivKey: %w", err) - } - if machinePrivKey.IsZero() { - return errors.New("getMachinePrivKey returned zero key") - } - - // TODO(maisem): dedupe this codepath from SetDNSNoise. - var serverNoiseKey key.MachinePublic - bodyData, err := encode(req, serverKey, serverNoiseKey, machinePrivKey) - if err != nil { - return err - } - body := bytes.NewReader(bodyData) - - u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().UntypedHexString()) - hreq, err := http.NewRequestWithContext(ctx, "POST", u, body) - if err != nil { - return err - } - res, err := c.httpc.Do(hreq) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != 200 { - msg, _ := io.ReadAll(res.Body) - return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg))) - } - var setDNSRes tailcfg.SetDNSResponse - if err := decode(res, &setDNSRes, serverKey, serverNoiseKey, machinePrivKey); err != nil { - c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err) - return fmt.Errorf("set-dns-response: %w", err) - } - - return nil + return c.setDNSNoise(ctx, req) } func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) { diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go index ca64a51dd..a23ee09c9 100644 --- a/tstest/integration/integration_test.go +++ b/tstest/integration/integration_test.go @@ -562,16 +562,11 @@ func TestAddPingRequest(t *testing.T) { func TestC2NPingRequest(t *testing.T) { tstest.Shard(t) tstest.Parallel(t) - env := newTestEnv(t) - n1 := newTestNode(t, env) - n1.StartDaemon() - n1.AwaitListening() - n1.MustUp() - n1.AwaitRunning() + env := newTestEnv(t) gotPing := make(chan bool, 1) - waitPing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + env.Control.HandleC2N = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Errorf("unexpected ping method %q", r.Method) } @@ -584,8 +579,14 @@ func TestC2NPingRequest(t *testing.T) { t.Errorf("body error\n got: %q\nwant: %q", got, want) } gotPing <- true - })) - defer waitPing.Close() + }) + + n1 := newTestNode(t, env) + n1.StartDaemon() + + n1.AwaitListening() + n1.MustUp() + n1.AwaitRunning() nodes := env.Control.AllNodes() if len(nodes) != 1 { @@ -604,7 +605,7 @@ func TestC2NPingRequest(t *testing.T) { cancel() pr := &tailcfg.PingRequest{ - URL: fmt.Sprintf("%s/ping-%d", waitPing.URL, try), + URL: fmt.Sprintf("https://unused/some-c2n-path/ping-%d", try), Log: true, Types: "c2n", Payload: []byte("POST /echo HTTP/1.0\r\nContent-Length: 3\r\n\r\nabc"), diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index c63d3bbc7..817af53d7 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -24,7 +24,8 @@ import ( "sync" "time" - "go4.org/mem" + "golang.org/x/net/http2" + "tailscale.com/control/controlhttp" "tailscale.com/net/netaddr" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" @@ -50,6 +51,7 @@ type Server struct { Verbose bool DNSConfig *tailcfg.DNSConfig // nil means no DNS config MagicDNSDomain string + HandleC2N http.Handler // if non-nil, used for /some-c2n-path/ in tests // ExplicitBaseURL or HTTPTestServer must be set. ExplicitBaseURL string // e.g. "http://127.0.0.1:1234" with no trailing URL @@ -82,7 +84,7 @@ type Server struct { suppressAutoMapResponses set.Set[key.NodePublic] noisePubKey key.MachinePublic - noisePrivKey key.ControlPrivate // not strictly needed vs. MachinePrivate, but handy to test type interactions. + noisePrivKey key.MachinePrivate nodes map[key.NodePublic]*tailcfg.Node users map[key.NodePublic]*tailcfg.User @@ -253,6 +255,10 @@ func (s *Server) initMux() { }) s.mux.HandleFunc("/key", s.serveKey) s.mux.HandleFunc("/machine/", s.serveMachine) + s.mux.HandleFunc("/ts2021", s.serveNoiseUpgrade) + if s.HandleC2N != nil { + s.mux.Handle("/some-c2n-path/", s.HandleC2N) + } } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -266,70 +272,87 @@ func (s *Server) serveUnhandled(w http.ResponseWriter, r *http.Request) { go panic(fmt.Sprintf("testcontrol.Server received unhandled request: %s", got.Bytes())) } -func (s *Server) publicKeys() (noiseKey, pubKey key.MachinePublic) { +type peerMachinePublicContextKey struct{} + +func (s *Server) serveNoiseUpgrade(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if r.Method != "POST" { + http.Error(w, "POST required", 400) + return + } + s.mu.Lock() - defer s.mu.Unlock() - s.ensureKeyPairLocked() - return s.noisePubKey, s.pubKey + noisePrivate := s.noisePrivKey + s.mu.Unlock() + cc, err := controlhttp.AcceptHTTP(ctx, w, r, noisePrivate, nil) + if err != nil { + log.Printf("AcceptHTTP: %v", err) + return + } + defer cc.Close() + + var h2srv http2.Server + peerPub := cc.Peer() + + h2srv.ServeConn(cc, &http2.ServeConnOpts{ + Context: context.WithValue(ctx, peerMachinePublicContextKey{}, peerPub), + BaseConfig: &http.Server{ + Handler: s.mux, + }, + }) } -func (s *Server) privateKey() key.ControlPrivate { +func (s *Server) publicKeys() (noiseKey, pubKey key.MachinePublic) { s.mu.Lock() defer s.mu.Unlock() s.ensureKeyPairLocked() - return s.privKey + return s.noisePubKey, s.pubKey } func (s *Server) ensureKeyPairLocked() { if !s.pubKey.IsZero() { return } - s.noisePrivKey = key.NewControl() + s.noisePrivKey = key.NewMachine() s.noisePubKey = s.noisePrivKey.Public() s.privKey = key.NewControl() s.pubKey = s.privKey.Public() } func (s *Server) serveKey(w http.ResponseWriter, r *http.Request) { - _, legacyKey := s.publicKeys() + noiseKey, legacyKey := s.publicKeys() if r.FormValue("v") == "" { w.Header().Set("Content-Type", "text/plain") io.WriteString(w, legacyKey.UntypedHexString()) return } w.Header().Set("Content-Type", "application/json") - // TODO(maisem/bradfitz): support noise protocol here. json.NewEncoder(w).Encode(&tailcfg.OverTLSPublicKeyResponse{ LegacyPublicKey: legacyKey, - // PublicKey: noiseKey, + PublicKey: noiseKey, }) } func (s *Server) serveMachine(w http.ResponseWriter, r *http.Request) { - mkeyStr := strings.TrimPrefix(r.URL.Path, "/machine/") - rem := "" - if i := strings.IndexByte(mkeyStr, '/'); i != -1 { - rem = mkeyStr[i:] - mkeyStr = mkeyStr[:i] - } - - // TODO(maisem/bradfitz): support noise protocol here. - mkey, err := key.ParseMachinePublicUntyped(mem.S(mkeyStr)) - if err != nil { - http.Error(w, "bad machine key hex", 400) - return - } - if r.Method != "POST" { http.Error(w, "POST required", 400) return } + ctx := r.Context() - switch rem { - case "": - s.serveRegister(w, r, mkey) - case "/map": + mkey, ok := ctx.Value(peerMachinePublicContextKey{}).(key.MachinePublic) + if !ok { + panic("no peer machine public key in context") + } + + switch r.URL.Path { + case "/machine/map": s.serveMap(w, r, mkey) + case "/machine/register": + s.serveRegister(w, r, mkey) + case "/machine/update-health": + io.Copy(io.Discard, r.Body) + w.WriteHeader(http.StatusNoContent) default: s.serveUnhandled(w, r) } @@ -549,7 +572,7 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key. } var req tailcfg.RegisterRequest - if err := s.decode(mkey, msg, &req); err != nil { + if err := s.decode(msg, &req); err != nil { go panic(fmt.Sprintf("serveRegister: decode: %v", err)) } if req.Version == 0 { @@ -563,7 +586,7 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key. log.Printf("Got %T: %s", req, j) } if s.RequireAuthKey != "" && req.Auth.AuthKey != s.RequireAuthKey { - res := must.Get(s.encode(mkey, false, tailcfg.RegisterResponse{ + res := must.Get(s.encode(false, tailcfg.RegisterResponse{ Error: "invalid authkey", })) w.WriteHeader(200) @@ -637,7 +660,7 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key. authURL = s.BaseURL() + authPath } - res, err := s.encode(mkey, false, tailcfg.RegisterResponse{ + res, err := s.encode(false, tailcfg.RegisterResponse{ User: *user, Login: *login, NodeKeyExpired: allExpired, @@ -729,7 +752,7 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.Machi r.Body.Close() req := new(tailcfg.MapRequest) - if err := s.decode(mkey, msg, req); err != nil { + if err := s.decode(msg, req); err != nil { go panic(fmt.Sprintf("bad map request: %v", err)) } @@ -1011,7 +1034,7 @@ func (s *Server) takeRawMapMessage(nk key.NodePublic) (mapResJSON []byte, ok boo } func (s *Server) sendMapMsg(w http.ResponseWriter, mkey key.MachinePublic, compress bool, msg any) error { - resBytes, err := s.encode(mkey, compress, msg) + resBytes, err := s.encode(compress, msg) if err != nil { return err } @@ -1034,19 +1057,14 @@ func (s *Server) sendMapMsg(w http.ResponseWriter, mkey key.MachinePublic, compr return nil } -func (s *Server) decode(mkey key.MachinePublic, msg []byte, v any) error { +func (s *Server) decode(msg []byte, v any) error { if len(msg) == msgLimit { return errors.New("encrypted message too long") } - - decrypted, ok := s.privateKey().OpenFrom(mkey, msg) - if !ok { - return errors.New("can't decrypt request") - } - return json.Unmarshal(decrypted, v) + return json.Unmarshal(msg, v) } -func (s *Server) encode(mkey key.MachinePublic, compress bool, v any) (b []byte, err error) { +func (s *Server) encode(compress bool, v any) (b []byte, err error) { var isBytes bool if b, isBytes = v.([]byte); !isBytes { b, err = json.Marshal(v) @@ -1057,7 +1075,7 @@ func (s *Server) encode(mkey key.MachinePublic, compress bool, v any) (b []byte, if compress { b = zstdframe.AppendEncode(nil, b, zstdframe.FastestCompression) } - return s.privateKey().SealTo(mkey, b), nil + return b, nil } // filterInvalidIPv6Endpoints removes invalid IPv6 endpoints from eps,