| // Copyright 2023 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //go:build !build_with_native_toolchain |
| |
| package netstack |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "syscall/zx" |
| "syscall/zx/fidl" |
| |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/fidlconv" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/routes" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/routetypes" |
| "go.fuchsia.dev/fuchsia/src/lib/component" |
| syslog "go.fuchsia.dev/fuchsia/src/lib/syslog/go" |
| "gvisor.dev/gvisor/pkg/tcpip" |
| |
| fuchsianet "fidl/fuchsia/net" |
| "fidl/fuchsia/net/interfaces/admin" |
| fnetRoutes "fidl/fuchsia/net/routes" |
| routesAdmin "fidl/fuchsia/net/routes/admin" |
| ) |
| |
| const ( |
| routeSetV4Name = "fuchsia.net.routes.admin/RouteSetV4" |
| routeSetV6Name = "fuchsia.net.routes.admin/RouteSetV6" |
| routesAdminName = "fuchsia.net.routes.admin" |
| |
| v4MainTableId = 0 |
| v6MainTableId = 1 |
| ) |
| |
| type UnauthenticatedError struct { |
| ID tcpip.NICID |
| } |
| |
| func (e *UnauthenticatedError) Error() string { |
| return fmt.Sprintf("Interface %v was not authenticated", e.ID) |
| } |
| |
| func (*UnauthenticatedError) Is(other error) bool { |
| _, ok := other.(*UnauthenticatedError) |
| return ok |
| } |
| |
| type routeSet[A fidlconv.IpAddress] struct { |
| ns *Netstack |
| id *routetypes.RouteSetId |
| authenticatedInterfaces map[tcpip.NICID]struct{} |
| } |
| |
| func makeUserRouteSet[A fidlconv.IpAddress](ns *Netstack) routeSet[A] { |
| return routeSet[A]{ |
| ns: ns, |
| id: &routetypes.RouteSetId{}, |
| authenticatedInterfaces: make(map[tcpip.NICID]struct{}), |
| } |
| } |
| |
| func makeGlobalRouteSet[A fidlconv.IpAddress](ns *Netstack) routeSet[A] { |
| return routeSet[A]{ |
| ns: ns, |
| id: routetypes.GlobalRouteSet(), |
| authenticatedInterfaces: make(map[tcpip.NICID]struct{}), |
| } |
| } |
| |
| // addRoute adds a route to the routeSet and returns whether the route is new |
| // to the routeSet. |
| func (r *routeSet[A]) addRoute(route fidlconv.Route[A]) (bool, error) { |
| nicID := tcpip.NICID(route.Action.Forward.OutboundInterface) |
| if _, ok := r.authenticatedInterfaces[nicID]; !ok { |
| return false, &UnauthenticatedError{nicID} |
| } |
| |
| gvisorRoute, err := route.GVisorRoute() |
| if err != nil { |
| return false, err |
| } |
| |
| metric, err := func() (*routetypes.Metric, error) { |
| switch route.Properties.Metric.Which() { |
| case fnetRoutes.SpecifiedMetricExplicitMetric: |
| metric := routetypes.Metric(route.Properties.Metric.ExplicitMetric) |
| return &metric, nil |
| case fnetRoutes.SpecifiedMetricInheritedFromInterface: |
| return nil, nil |
| default: |
| return nil, fmt.Errorf("unknown metric type: %+v", route.Properties.Metric) |
| } |
| }() |
| |
| if err != nil { |
| return false, err |
| } |
| |
| addResult, err := r.ns.AddRoute(gvisorRoute, metric, false /* dynamic */, false /* replaceMatchingGvisorRoutes */, r.id) |
| return addResult.NewlyAddedToSet, err |
| } |
| |
| // removesRoute removes a route from the routeSet and returns whether the route |
| // was present in the routeSet. |
| func (r *routeSet[A]) removeRoute(route fidlconv.Route[A]) (bool, error) { |
| nicID := tcpip.NICID(route.Action.Forward.OutboundInterface) |
| if _, ok := r.authenticatedInterfaces[nicID]; !ok { |
| return false, &UnauthenticatedError{nicID} |
| } |
| |
| gvisorRoute, err := route.GVisorRoute() |
| if err != nil { |
| return false, err |
| } |
| |
| metric, err := func() (*routetypes.Metric, error) { |
| switch route.Properties.Metric.Which() { |
| case fnetRoutes.SpecifiedMetricExplicitMetric: |
| metric := routetypes.Metric(route.Properties.Metric.ExplicitMetric) |
| return &metric, nil |
| case fnetRoutes.SpecifiedMetricInheritedFromInterface: |
| return nil, nil |
| default: |
| return nil, fmt.Errorf("unknown metric type: %+v", route.Properties.Metric) |
| } |
| }() |
| |
| if err != nil { |
| return false, err |
| } |
| |
| removeResult, err := r.ns.DelRouteExactMatch(gvisorRoute, metric, r.id) |
| return removeResult.NewlyRemovedFromSet, err |
| } |
| |
| // close deletes the routeSet in the Netstack. |
| func (r *routeSet[A]) close() { |
| r.ns.DelRouteSet(r.id) |
| } |
| |
| type routesAdminMainRouteTable struct { |
| tableId uint32 |
| token zx.Event |
| } |
| |
| func (impl *routesAdminMainRouteTable) GetTableId(ctx_ fidl.Context) (uint32, error) { |
| return impl.tableId, nil |
| } |
| |
| func (impl *routesAdminMainRouteTable) Detach(ctx_ fidl.Context) error { |
| return nil |
| } |
| |
| func (impl *routesAdminMainRouteTable) Remove(ctx_ fidl.Context) (routesAdmin.BaseRouteTableRemoveResult, error) { |
| return routesAdmin.BaseRouteTableRemoveResultWithErr(routesAdmin.BaseRouteTableRemoveErrorInvalidOpOnMainTable), nil |
| } |
| |
| func (impl *routesAdminMainRouteTable) GetAuthorizationForRouteTable(ctx_ fidl.Context) (uint32, zx.Event, error) { |
| token, err := impl.token.Duplicate(zx.RightTransfer | zx.RightDuplicate) |
| if err != nil { |
| return 0, token, err |
| } |
| return impl.tableId, token, nil |
| } |
| |
| var _ routesAdmin.BaseRouteTableWithCtx = (*routesAdminMainRouteTable)(nil) |
| |
| var _ routesAdmin.RouteTableV4WithCtx = (*routesAdminRouteTableV4Impl)(nil) |
| |
| type routesAdminRouteTableV4Impl struct { |
| routesAdminMainRouteTable |
| ns *Netstack |
| } |
| |
| func (impl *routesAdminRouteTableV4Impl) NewRouteSet(ctx_ fidl.Context, request routesAdmin.RouteSetV4WithCtxInterfaceRequest) error { |
| return bindV4RouteSet(request.Channel, makeUserRouteSet[fuchsianet.Ipv4Address](impl.ns)) |
| } |
| |
| func bindV4RouteSet(ch zx.Channel, rs routeSet[fuchsianet.Ipv4Address]) error { |
| ctx, cancel := context.WithCancel(context.Background()) |
| |
| go func() { |
| defer cancel() |
| |
| setImpl := routeSetV4Impl{ |
| routeSet: rs, |
| } |
| defer setImpl.routeSet.close() |
| |
| component.Serve( |
| ctx, |
| &routesAdmin.RouteSetV4WithCtxStub{Impl: &setImpl}, |
| ch, |
| component.ServeOptions{ |
| Concurrent: true, |
| OnError: func(err error) { |
| _ = syslog.ErrorTf(routeSetV4Name, "%s", err) |
| }, |
| }, |
| ) |
| }() |
| |
| return nil |
| } |
| |
| var _ routesAdmin.RouteTableV6WithCtx = (*routesAdminRouteTableV6Impl)(nil) |
| |
| type routesAdminRouteTableV6Impl struct { |
| routesAdminMainRouteTable |
| ns *Netstack |
| } |
| |
| func (impl *routesAdminRouteTableV6Impl) NewRouteSet(ctx_ fidl.Context, request routesAdmin.RouteSetV6WithCtxInterfaceRequest) error { |
| return bindV6RouteSet(request.Channel, makeUserRouteSet[fuchsianet.Ipv6Address](impl.ns)) |
| } |
| |
| func bindV6RouteSet(ch zx.Channel, rs routeSet[fuchsianet.Ipv6Address]) error { |
| ctx, cancel := context.WithCancel(context.Background()) |
| |
| go func() { |
| defer cancel() |
| |
| setImpl := routeSetV6Impl{ |
| routeSet: rs, |
| } |
| defer setImpl.routeSet.close() |
| |
| component.Serve( |
| ctx, |
| &routesAdmin.RouteSetV6WithCtxStub{Impl: &setImpl}, |
| ch, |
| component.ServeOptions{ |
| Concurrent: true, |
| OnError: func(err error) { |
| _ = syslog.ErrorTf(routeSetV6Name, "%s", err) |
| }, |
| }, |
| ) |
| }() |
| |
| return nil |
| } |
| |
| func validateInterfaceCredential(ns *Netstack, clientCredential admin.ProofOfInterfaceAuthorization) bool { |
| nsIfInfo, ok := ns.stack.NICInfo()[tcpip.NICID(clientCredential.InterfaceId)] |
| if !ok { |
| // A NIC not existing should be transformed into INVALID_AUTHENTICATION, |
| // just like an invalid credential. |
| return false |
| } |
| |
| nsTokenInfo, err := nsIfInfo.Context.(*ifState).authorizationToken.Handle().GetInfoHandleBasic() |
| if err != nil { |
| panic(fmt.Sprintf("Failed to get basic info for Netstack-generated token: %s", err)) |
| } |
| |
| clientTokenInfo, err := clientCredential.Token.Handle().GetInfoHandleBasic() |
| if err != nil { |
| _ = syslog.ErrorTf(routesAdminName, "while getting handle info for credential object: %s", err) |
| return false |
| } |
| |
| return nsTokenInfo.Koid == clientTokenInfo.Koid |
| } |
| |
| type routeSetV4Impl struct { |
| routeSet routeSet[fuchsianet.Ipv4Address] |
| } |
| |
| var _ routesAdmin.RouteSetV4WithCtx = (*routeSetV4Impl)(nil) |
| |
| func (r *routeSetV4Impl) AddRoute(ctx_ fidl.Context, fidlRoute fnetRoutes.RouteV4) (routesAdmin.RouteSetV4AddRouteResult, error) { |
| var result routesAdmin.RouteSetV4AddRouteResult |
| route, validationResult := fidlconv.FromFidlRouteV4(fidlRoute) |
| |
| switch validationResult { |
| case fidlconv.RouteOk: |
| didAdd, err := r.routeSet.addRoute(route) |
| if err != nil { |
| if errors.Is(err, routes.ErrNoSuchNIC) { |
| result.SetErr(routesAdmin.RouteSetErrorPreviouslyAuthenticatedInterfaceNoLongerExists) |
| return result, nil |
| } else if errors.Is(err, &UnauthenticatedError{}) { |
| return routesAdmin.RouteSetV4AddRouteResultWithErr(routesAdmin.RouteSetErrorUnauthenticated), nil |
| } else { |
| _ = syslog.ErrorTf(routeSetV4Name, "error while adding route: %s", err) |
| } |
| } |
| result.SetResponse(routesAdmin.RouteSetV4AddRouteResponse{ |
| DidAdd: didAdd, |
| }) |
| case fidlconv.RouteInvalidDestinationSubnet: |
| result.SetErr(routesAdmin.RouteSetErrorInvalidDestinationSubnet) |
| case fidlconv.RouteInvalidNextHop: |
| result.SetErr(routesAdmin.RouteSetErrorInvalidNextHop) |
| case fidlconv.RouteInvalidUnknownAction: |
| result.SetErr(routesAdmin.RouteSetErrorUnsupportedAction) |
| case fidlconv.RouteInvalidMissingRouteProperties: |
| result.SetErr(routesAdmin.RouteSetErrorMissingRouteProperties) |
| case fidlconv.RouteInvalidMissingMetric: |
| result.SetErr(routesAdmin.RouteSetErrorMissingMetric) |
| default: |
| panic(fmt.Sprintf("unrecognized RouteValidationResult: %d", validationResult)) |
| } |
| |
| return result, nil |
| } |
| |
| func (r *routeSetV4Impl) AuthenticateForInterface(ctx_ fidl.Context, credential admin.ProofOfInterfaceAuthorization) (routesAdmin.RouteSetV4AuthenticateForInterfaceResult, error) { |
| defer func() { |
| // Close credential token to prevent resource leaks. |
| if err := credential.Token.Close(); err != nil { |
| _ = syslog.WarnTf(routeSetV6Name, "failed to close credential: %s", err) |
| } |
| }() |
| |
| var result routesAdmin.RouteSetV4AuthenticateForInterfaceResult |
| if validateInterfaceCredential(r.routeSet.ns, credential) { |
| r.routeSet.authenticatedInterfaces[tcpip.NICID(credential.InterfaceId)] = struct{}{} |
| result.SetResponse(routesAdmin.RouteSetV4AuthenticateForInterfaceResponse{}) |
| } else { |
| result.SetErr(routesAdmin.AuthenticateForInterfaceErrorInvalidAuthentication) |
| } |
| |
| return result, nil |
| } |
| |
| func (r *routeSetV4Impl) RemoveRoute(ctx_ fidl.Context, fidlRoute fnetRoutes.RouteV4) (routesAdmin.RouteSetV4RemoveRouteResult, error) { |
| var result routesAdmin.RouteSetV4RemoveRouteResult |
| route, validationResult := fidlconv.FromFidlRouteV4(fidlRoute) |
| |
| switch validationResult { |
| case fidlconv.RouteOk: |
| didRemove, err := r.routeSet.removeRoute(route) |
| if err != nil { |
| if errors.Is(err, routes.ErrNoSuchNIC) { |
| result.SetErr(routesAdmin.RouteSetErrorPreviouslyAuthenticatedInterfaceNoLongerExists) |
| return result, nil |
| } else if errors.Is(err, &UnauthenticatedError{}) { |
| return routesAdmin.RouteSetV4RemoveRouteResultWithErr(routesAdmin.RouteSetErrorUnauthenticated), nil |
| } else { |
| _ = syslog.ErrorTf(routeSetV4Name, "error while removing route: %s", err) |
| } |
| } |
| result.SetResponse(routesAdmin.RouteSetV4RemoveRouteResponse{ |
| DidRemove: didRemove, |
| }) |
| case fidlconv.RouteInvalidDestinationSubnet: |
| result.SetErr(routesAdmin.RouteSetErrorInvalidDestinationSubnet) |
| case fidlconv.RouteInvalidNextHop: |
| result.SetErr(routesAdmin.RouteSetErrorInvalidNextHop) |
| case fidlconv.RouteInvalidUnknownAction: |
| result.SetErr(routesAdmin.RouteSetErrorUnsupportedAction) |
| case fidlconv.RouteInvalidMissingRouteProperties: |
| result.SetErr(routesAdmin.RouteSetErrorMissingRouteProperties) |
| case fidlconv.RouteInvalidMissingMetric: |
| result.SetErr(routesAdmin.RouteSetErrorMissingMetric) |
| default: |
| panic(fmt.Sprintf("unrecognized RouteValidationResult: %d", validationResult)) |
| } |
| |
| return result, nil |
| } |
| |
| type routeSetV6Impl struct { |
| routeSet routeSet[fuchsianet.Ipv6Address] |
| } |
| |
| var _ routesAdmin.RouteSetV6WithCtx = (*routeSetV6Impl)(nil) |
| |
| func (r *routeSetV6Impl) AddRoute(ctx_ fidl.Context, fidlRoute fnetRoutes.RouteV6) (routesAdmin.RouteSetV6AddRouteResult, error) { |
| var result routesAdmin.RouteSetV6AddRouteResult |
| route, validationResult := fidlconv.FromFidlRouteV6(fidlRoute) |
| |
| switch validationResult { |
| case fidlconv.RouteOk: |
| didAdd, err := r.routeSet.addRoute(route) |
| if err != nil { |
| if errors.Is(err, routes.ErrNoSuchNIC) { |
| result.SetErr(routesAdmin.RouteSetErrorUnauthenticated) |
| return result, nil |
| } else if errors.Is(err, &UnauthenticatedError{}) { |
| return routesAdmin.RouteSetV6AddRouteResultWithErr(routesAdmin.RouteSetErrorUnauthenticated), nil |
| } else { |
| _ = syslog.ErrorTf(routeSetV6Name, "error while adding route: %s", err) |
| } |
| } |
| result.SetResponse(routesAdmin.RouteSetV6AddRouteResponse{ |
| DidAdd: didAdd, |
| }) |
| |
| case fidlconv.RouteInvalidDestinationSubnet: |
| result.SetErr(routesAdmin.RouteSetErrorInvalidDestinationSubnet) |
| case fidlconv.RouteInvalidNextHop: |
| _ = syslog.ErrorTf(routeSetV6Name, "rejecting route %#v due to invalid next hop", route) |
| result.SetErr(routesAdmin.RouteSetErrorInvalidNextHop) |
| case fidlconv.RouteInvalidUnknownAction: |
| result.SetErr(routesAdmin.RouteSetErrorUnsupportedAction) |
| case fidlconv.RouteInvalidMissingRouteProperties: |
| result.SetErr(routesAdmin.RouteSetErrorMissingRouteProperties) |
| case fidlconv.RouteInvalidMissingMetric: |
| result.SetErr(routesAdmin.RouteSetErrorMissingMetric) |
| default: |
| panic(fmt.Sprintf("unrecognized RouteValidationResult: %d", validationResult)) |
| } |
| |
| return result, nil |
| } |
| |
| func (r *routeSetV6Impl) AuthenticateForInterface(ctx_ fidl.Context, credential admin.ProofOfInterfaceAuthorization) (routesAdmin.RouteSetV6AuthenticateForInterfaceResult, error) { |
| defer func() { |
| // Close credential token to prevent resource leaks. |
| if err := credential.Token.Close(); err != nil { |
| _ = syslog.WarnTf(routeSetV6Name, "failed to close credential: %s", err) |
| } |
| }() |
| |
| var result routesAdmin.RouteSetV6AuthenticateForInterfaceResult |
| if validateInterfaceCredential(r.routeSet.ns, credential) { |
| r.routeSet.authenticatedInterfaces[tcpip.NICID(credential.InterfaceId)] = struct{}{} |
| result.SetResponse(routesAdmin.RouteSetV6AuthenticateForInterfaceResponse{}) |
| } else { |
| result.SetErr(routesAdmin.AuthenticateForInterfaceErrorInvalidAuthentication) |
| } |
| |
| return result, nil |
| } |
| |
| func (r *routeSetV6Impl) RemoveRoute(ctx_ fidl.Context, fidlRoute fnetRoutes.RouteV6) (routesAdmin.RouteSetV6RemoveRouteResult, error) { |
| var result routesAdmin.RouteSetV6RemoveRouteResult |
| route, validationResult := fidlconv.FromFidlRouteV6(fidlRoute) |
| |
| switch validationResult { |
| case fidlconv.RouteOk: |
| didRemove, err := r.routeSet.removeRoute(route) |
| if err != nil { |
| if errors.Is(err, routes.ErrNoSuchNIC) { |
| result.SetErr(routesAdmin.RouteSetErrorUnauthenticated) |
| return result, nil |
| } else if errors.Is(err, &UnauthenticatedError{}) { |
| return routesAdmin.RouteSetV6RemoveRouteResultWithErr(routesAdmin.RouteSetErrorUnauthenticated), nil |
| } else { |
| _ = syslog.ErrorTf(routeSetV6Name, "error while removing route: %s", err) |
| } |
| } |
| result.SetResponse(routesAdmin.RouteSetV6RemoveRouteResponse{ |
| DidRemove: didRemove, |
| }) |
| case fidlconv.RouteInvalidDestinationSubnet: |
| result.SetErr(routesAdmin.RouteSetErrorInvalidDestinationSubnet) |
| case fidlconv.RouteInvalidNextHop: |
| result.SetErr(routesAdmin.RouteSetErrorInvalidNextHop) |
| case fidlconv.RouteInvalidUnknownAction: |
| result.SetErr(routesAdmin.RouteSetErrorUnsupportedAction) |
| case fidlconv.RouteInvalidMissingRouteProperties: |
| result.SetErr(routesAdmin.RouteSetErrorMissingRouteProperties) |
| case fidlconv.RouteInvalidMissingMetric: |
| result.SetErr(routesAdmin.RouteSetErrorMissingMetric) |
| default: |
| panic(fmt.Sprintf("unrecognized RouteValidationResult: %d", validationResult)) |
| } |
| |
| return result, nil |
| } |