mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 00:35:41 +00:00
67eba590e2
Implements the router-side Connect-V2 sessionless dial path. Dials are authorized locally via the RouterDataModel instead of a controller-issued service session token; circuit creation flows through the existing `CreateCircuitV3` controller endpoint (#3721). Builds on the sdk-golang v2 migration. - Adds `processConnectV2` on `edgeClientConn`: resolves the service by id or name via the RouterDataModel, checks dial access, and dispatches to the controller via `sendCreateCircuitV3Msg`. Supports both `xgEdgeForwarder` (SDK xgress) and `nonXgConnectHandler` flow-control modes, selected by the SDK's `UseXgressToSdkHeader`. - Makes `CircuitId` optional in `DecodeCreateCircuitV3Request`. The V2 router path does not pre-assign a circuit ID; the controller generates it as V1/V2 already do. Without this the decoder rejected the empty header and every V2 dial hung until timeout. Adds a regression test. - Splits `checkAccess` to close a posture-check bypass on the V2 path. The old single `checkAccess` short-circuited to nil for non-OIDC sessions (V1 ran posture at the controller during `CreateSession`); V2 has no such step, so posture would have been skipped. `checkAccess` now always runs the RDM `HasAccess` (policy + posture) check; `checkAccessIfOidc` keeps the OIDC-only gate for the V1 and bind paths. - Sends the V2 `state_connected` on the default (data) sender rather than the control sender. On multi-underlay channels the two senders are independently ordered, so an early terminator payload on the data sender could beat `state_connected` to the SDK and be dropped (channel/v5 has no message-priority API). - Updates `xgEdgeForwarder.lastRx` on every forward path, including the fast `timeout == 0` `TrySend` branch used for normal payload dispatch. The old code only updated it on the `timeout > 0` path, so active V2 circuits looked idle and could be unrouted prematurely. - Adds `state.ConnState.ServiceId`, populated by the connect handlers from the service session token (V1) or the request header (V2). The non-xgress V2 path previously left this empty, so `handleDialAccessLost` could not identify and close V2 non-xgress circuits when dial access was revoked. - Skips conns with no `ServiceSessionToken` in `RemoveLegacyServiceSession`; a sessionless V2 conn's token is nil and the cleanup loop previously dereferenced it unconditionally, which would panic the router. - Advertises Connect-V2 via the `RouterCapabilityConnectV2` bit in the listener hello so SDKs can detect V2 support. - Wires `ContentTypeConnectV2` and `ContentTypeXgControl` handlers in `Acceptor.BindChannel`, and adds `handleXgControl` for SDK-side xgress control messages, preserving `ControlUserVal` so trace-route responses correlate back to the initiator's `SendForReply` waiter. - Adds `RouterDataModel.serviceNameIndex` for O(1) name->id lookup in the V2 dial path, maintained with rename safety at the `HandleServiceEvent` mutation points. - Adds `tests/connect_v2_test.go` covering end-to-end V2 dataflow and the V1 fallback (`ForceConnectV1`), asserting the dial path via the SDK `DialEvent`. - Propagates a V2 initiator's graceful half-close to legacy hosts via `edgeXgressConn.FlowFromFabricToXgressClosed`, which emits an edge FIN when the fabric->app half of the circuit closes. The SDK signals half-close to its router xgress peer with the native xgress EOF flag; without translating that to an edge FIN, a legacy host reading to EOF stalled until teardown. - Records the dialing identity id as the circuit `ClientId` for sessionless V2 dials, since there is no dial session to key on; updates `Test_OidcEvents` to match. - Adds `tests/connect_v2_teardown_test.go` covering client- and host-initiated close propagation on both the V2 and forced-V1 paths. - Polls for the asynchronous conn close in the SDK posture-check tests (`awaitClientConnClosed`): revocation tears the circuit down out of band, so checking `IsClosed` immediately after the first read error was racy. - Temporarily pins sdk-golang/v2 to the openziti/sdk-golang#959 commit, which carries the matching xgress conn-close-on-teardown fix the V2 posture tests depend on; to be repointed at the next sdk-golang pre-release before merge. For openziti/sdk-golang#936.
171 lines
5.1 KiB
Go
171 lines
5.1 KiB
Go
//go:build dataflow
|
|
|
|
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package tests
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/openziti/sdk-golang/v2/ziti"
|
|
"github.com/openziti/sdk-golang/v2/ziti/edge"
|
|
"github.com/openziti/ziti/v2/common/eid"
|
|
"github.com/openziti/ziti/v2/controller/xt_smartrouting"
|
|
)
|
|
|
|
// Test_ConnectV2_Dataflow exercises the sessionless ConnectV2 dial path
|
|
// end-to-end. The SDK defaults to V2 whenever the router advertises the
|
|
// capability and `ForceConnectV1` is not set. The dial protocol is asserted
|
|
// explicitly via the DialEvent so a capability/auth negotiation regression
|
|
// fails directly rather than only as a hang or data failure.
|
|
func Test_ConnectV2_Dataflow(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
service := ctx.AdminManagementSession.RequireNewServiceAccessibleToAll(xt_smartrouting.Name)
|
|
|
|
ctx.CreateEnrollAndStartEdgeRouter()
|
|
_, hostContext := ctx.AdminManagementSession.RequireCreateSdkContext()
|
|
defer hostContext.Close()
|
|
|
|
listener, err := hostContext.Listen(service.Name)
|
|
ctx.Req.NoError(err)
|
|
defer listener.Close()
|
|
|
|
testServer := newTestServer(listener, func(conn *testServerConn) error {
|
|
for {
|
|
name, eof := conn.ReadString(math.MaxUint16*4, time.Minute)
|
|
if eof {
|
|
return conn.server.close()
|
|
}
|
|
if name == "quit" {
|
|
conn.WriteString("ok", time.Second)
|
|
return conn.server.close()
|
|
}
|
|
conn.WriteString("hello, "+name, time.Second)
|
|
}
|
|
})
|
|
testServer.start()
|
|
|
|
_, clientContext := ctx.AdminManagementSession.RequireCreateSdkContext()
|
|
defer clientContext.Close()
|
|
|
|
var dialEvt ziti.DialEvent
|
|
dialEvtSet := false
|
|
removeListener := clientContext.Events().AddDialListener(func(_ ziti.Context, evt ziti.DialEvent) {
|
|
if evt.ServiceName == service.Name {
|
|
dialEvt = evt
|
|
dialEvtSet = true
|
|
}
|
|
})
|
|
defer removeListener()
|
|
|
|
dialOptions := &ziti.DialOptions{
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
conn := ctx.WrapConn(clientContext.DialWithOptions(service.Name, dialOptions))
|
|
defer conn.Close()
|
|
|
|
ctx.Req.True(dialEvtSet, "expected a dial event for service %s", service.Name)
|
|
ctx.Req.Equal(edge.DialProtocolConnectV2, dialEvt.Protocol, "expected the dial to take the ConnectV2 path")
|
|
|
|
name := eid.New()
|
|
conn.WriteString(name, time.Second)
|
|
conn.ReadExpected("hello, "+name, time.Second)
|
|
|
|
conn.WriteString("quit", time.Second)
|
|
conn.ReadExpected("ok", time.Second)
|
|
|
|
testServer.waitForDone(ctx, 5*time.Second)
|
|
}
|
|
|
|
// Test_ConnectV1_Fallback_Dataflow confirms that the V1 fallback path still
|
|
// works after the connect-v2 changes — important because the SDK still uses
|
|
// V1 against routers that don't advertise V2, and the ForceConnectV1 escape
|
|
// hatch is a documented supported option.
|
|
func Test_ConnectV1_Fallback_Dataflow(t *testing.T) {
|
|
ctx := NewTestContext(t)
|
|
defer ctx.Teardown()
|
|
ctx.StartServer()
|
|
ctx.RequireAdminManagementApiLogin()
|
|
|
|
service := ctx.AdminManagementSession.RequireNewServiceAccessibleToAll(xt_smartrouting.Name)
|
|
|
|
ctx.CreateEnrollAndStartEdgeRouter()
|
|
_, hostContext := ctx.AdminManagementSession.RequireCreateSdkContext()
|
|
defer hostContext.Close()
|
|
|
|
listener, err := hostContext.Listen(service.Name)
|
|
ctx.Req.NoError(err)
|
|
defer listener.Close()
|
|
|
|
testServer := newTestServer(listener, func(conn *testServerConn) error {
|
|
for {
|
|
name, eof := conn.ReadString(math.MaxUint16*4, time.Minute)
|
|
if eof {
|
|
return conn.server.close()
|
|
}
|
|
if name == "quit" {
|
|
conn.WriteString("ok", time.Second)
|
|
return conn.server.close()
|
|
}
|
|
conn.WriteString("hello, "+name, time.Second)
|
|
}
|
|
})
|
|
testServer.start()
|
|
|
|
_, clientContext := ctx.AdminManagementSession.RequireCreateSdkContext()
|
|
defer clientContext.Close()
|
|
|
|
var dialEvt ziti.DialEvent
|
|
dialEvtSet := false
|
|
removeListener := clientContext.Events().AddDialListener(func(_ ziti.Context, evt ziti.DialEvent) {
|
|
if evt.ServiceName == service.Name {
|
|
dialEvt = evt
|
|
dialEvtSet = true
|
|
}
|
|
})
|
|
defer removeListener()
|
|
|
|
forceV1 := true
|
|
dialOptions := &ziti.DialOptions{
|
|
ConnectTimeout: 5 * time.Second,
|
|
ForceConnectV1: &forceV1,
|
|
}
|
|
|
|
conn := ctx.WrapConn(clientContext.DialWithOptions(service.Name, dialOptions))
|
|
defer conn.Close()
|
|
|
|
ctx.Req.True(dialEvtSet, "expected a dial event for service %s", service.Name)
|
|
ctx.Req.Equal(edge.DialProtocolConnectV1, dialEvt.Protocol, "expected the dial to take the ConnectV1 fallback path")
|
|
ctx.Req.True(dialEvt.Forced, "expected the V1 dial to be flagged as forced via ForceConnectV1")
|
|
|
|
name := eid.New()
|
|
conn.WriteString(name, time.Second)
|
|
conn.ReadExpected("hello, "+name, time.Second)
|
|
|
|
conn.WriteString("quit", time.Second)
|
|
conn.ReadExpected("ok", time.Second)
|
|
|
|
testServer.waitForDone(ctx, 5*time.Second)
|
|
}
|