Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dataplane/docs/dataplanedebug.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@

The lemctl CLI contains useful tools for debugging Lemming and Lucius.

Live `lemctl lucius info` and `lemctl lucius sai` queries verify TLS certificates
by default. Use `--tls_ca_cert ca.pem` to add a private CA to system trust roots;
`--tls_skip_verify` is no longer supported. Plaintext `--insecure` requires a
literal loopback IP and port, such as `127.0.0.1:50000` or `[::1]:50000`, and
connects directly without a proxy. Hostnames, remote IPs, resolver targets, Unix
sockets and scoped IPv6 addresses are rejected for plaintext. `--tls_ca_cert`
cannot be combined with `--insecure`.

To enable packet traces, the lucius containers must be logging at level 3 or
higher. Add the "-v=3" arg in the topology file for all lucius containers.

Expand Down
18 changes: 17 additions & 1 deletion dataplane/luciusctl/BUILD
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "luciusctl",
Expand All @@ -12,3 +12,19 @@ go_library(
"@com_github_spf13_viper//:viper",
],
)

go_test(
name = "luciusctl_test",
size = "small",
srcs = ["luciusctl_test.go"],
embed = [":luciusctl"],
# keep: Cobra and Viper are inherited from :luciusctl.
deps = [
"//dataplane/proto/sai",
"//proto/forwarding",
"@org_golang_google_grpc//:grpc",
"@org_golang_google_grpc//credentials",
"@org_golang_google_grpc//peer",
"@org_golang_google_grpc//reflection",
],
)
5 changes: 1 addition & 4 deletions dataplane/luciusctl/info/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ go_library(
importpath = "github.com/openconfig/lemming/dataplane/luciusctl/info",
visibility = ["//visibility:public"],
deps = [
"//dataplane/luciusctl/internal/transport",
"//proto/forwarding",
"@com_github_spf13_cobra//:cobra",
"@com_github_spf13_viper//:viper",
"@org_golang_google_grpc//:grpc",
"@org_golang_google_grpc//credentials",
"@org_golang_google_grpc//credentials/insecure",
],
)
26 changes: 4 additions & 22 deletions dataplane/luciusctl/info/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@ package info

import (
"context"
"crypto/tls"
"encoding/hex"
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"

"github.com/openconfig/lemming/dataplane/luciusctl/internal/transport"

fwdpb "github.com/openconfig/lemming/proto/forwarding"
)
Expand Down Expand Up @@ -73,7 +70,7 @@ func New() *cobra.Command {
}

func listFn(cmd *cobra.Command, _ []string) error {
conn, err := dial()
conn, err := transport.Dial()
if err != nil {
return fmt.Errorf("failed to dial dataplane: %v", err)
}
Expand All @@ -92,7 +89,7 @@ func listFn(cmd *cobra.Command, _ []string) error {
}

func infoElement(ctx context.Context, name string, infoType fwdpb.InfoType, packet string) error {
conn, err := dial()
conn, err := transport.Dial()
if err != nil {
return fmt.Errorf("failed to dial dataplane: %v", err)
}
Expand All @@ -116,18 +113,3 @@ func infoElement(ctx context.Context, name string, infoType fwdpb.InfoType, pack
fmt.Println(resp.GetContent())
return nil
}

func dial() (*grpc.ClientConn, error) {
insec, tlsSkipVerify := viper.GetBool("insecure"), viper.GetBool("tls_skip_verify")
if insec && tlsSkipVerify {
return nil, fmt.Errorf("both insecure and tls skip verify are set")
}
opts := grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
InsecureSkipVerify: tlsSkipVerify, // nolint:gosec
}))
if insec {
opts = grpc.WithTransportCredentials(insecure.NewCredentials())
}

return grpc.NewClient(viper.GetString("address"), opts)
}
23 changes: 23 additions & 0 deletions dataplane/luciusctl/internal/transport/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "transport",
srcs = ["transport.go"],
importpath = "github.com/openconfig/lemming/dataplane/luciusctl/internal/transport",
visibility = ["//dataplane/luciusctl:__subpackages__"],
deps = [
"@com_github_spf13_viper//:viper",
"@org_golang_google_grpc//:grpc",
"@org_golang_google_grpc//credentials",
"@org_golang_google_grpc//credentials/insecure",
],
)

go_test(
name = "transport_test",
size = "small",
srcs = ["transport_test.go"],
embed = [":transport"],
# keep: Viper is inherited from :transport.
deps = [],
)
94 changes: 94 additions & 0 deletions dataplane/luciusctl/internal/transport/transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2026 Microsoft Corporation
//
// 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
//
// http://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 transport provides verified TLS or local-only plaintext connections.
package transport

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/netip"
"os"
"strconv"

"github.com/spf13/viper"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
)

// Dial creates a channel using the configured diagnostic transport policy.
func Dial() (*grpc.ClientConn, error) {
if viper.GetBool("tls_skip_verify") {
return nil, fmt.Errorf("--tls_skip_verify is no longer supported; use --tls_ca_cert to trust the server's CA")
}
address := viper.GetString("address")
if address == "" {
return nil, fmt.Errorf("--address is required")
}
caFile := viper.GetString("tls_ca_cert")
if viper.GetBool("insecure") {
if caFile != "" {
return nil, fmt.Errorf("--tls_ca_cert cannot be used with --insecure")
}
address, err := loopbackAddress(address)
if err != nil {
return nil, err
}
// Pin every connection to the validated IP, bypassing resolvers and proxies.
dialer := &net.Dialer{}
return grpc.NewClient("passthrough:///"+address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", address)
}),
)
}

config := &tls.Config{MinVersion: tls.VersionTLS12}
if caFile != "" {
certs, err := os.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("read --tls_ca_cert: %w", err)
}
roots, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("load system certificate roots: %w", err)
}
if !roots.AppendCertsFromPEM(certs) {
return nil, fmt.Errorf("--tls_ca_cert contains no valid PEM certificates")
}
config.RootCAs = roots
}
return grpc.NewClient(address, grpc.WithTransportCredentials(credentials.NewTLS(config)))
}

func loopbackAddress(address string) (string, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return "", fmt.Errorf("--insecure requires a literal loopback IP and port: %w", err)
}
ip, err := netip.ParseAddr(host)
if err != nil || ip.Zone() != "" || !ip.Unmap().IsLoopback() {
return "", fmt.Errorf("--insecure requires a literal loopback IP; hostnames, remote IPs and resolver targets are not supported")
}
number, err := strconv.ParseUint(port, 10, 16)
if err != nil || number == 0 {
return "", fmt.Errorf("--insecure requires a port between 1 and 65535")
}
return net.JoinHostPort(ip.Unmap().String(), strconv.FormatUint(number, 10)), nil
}
115 changes: 115 additions & 0 deletions dataplane/luciusctl/internal/transport/transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2026 Microsoft Corporation
//
// 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
//
// http://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 transport

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/spf13/viper"
)

func TestLoopbackAddress(t *testing.T) {
tests := []struct {
address string
want string
}{
{address: "127.0.0.1:50000", want: "127.0.0.1:50000"},
{address: "127.255.255.254:1", want: "127.255.255.254:1"},
{address: "[::1]:65535", want: "[::1]:65535"},
{address: "[0:0:0:0:0:0:0:1]:05000", want: "[::1]:5000"},
{address: "[::ffff:127.0.0.1]:50000", want: "127.0.0.1:50000"},
{address: "localhost:50000"},
{address: "localhost.:50000"},
{address: "127.0.0.1.example:50000"},
{address: "192.0.2.1:50000"},
{address: "10.0.0.1:50000"},
{address: "0.0.0.0:50000"},
{address: "[::]:50000"},
{address: "[2001:db8::1]:50000"},
{address: "[::ffff:192.0.2.1]:50000"},
{address: "[::1%lo]:50000"},
{address: "dns:///127.0.0.1:50000"},
{address: "passthrough:///127.0.0.1:50000"},
{address: "unix:///tmp/lucius.sock"},
{address: "http://127.0.0.1:50000"},
{address: "127.1:50000"},
{address: "2130706433:50000"},
{address: "0177.0.0.1:50000"},
{address: "127.0.0.1"},
{address: "::1:50000"},
{address: "127.0.0.1:0"},
{address: "127.0.0.1:65536"},
{address: "127.0.0.1:http"},
{address: "127.0.0.1:+1"},
{address: "127.0.0.1:-1"},
{address: "127.0.0.1:"},
{address: ""},
}
for _, test := range tests {
t.Run(test.address, func(t *testing.T) {
got, err := loopbackAddress(test.address)
if test.want == "" {
if err == nil {
t.Fatalf("accepted prohibited plaintext target %q as %q", test.address, got)
}
return
}
if err != nil || got != test.want {
t.Fatalf("loopbackAddress(%q) = %q, %v; want %q", test.address, got, err, test.want)
}
})
}
}

func TestDialConfigurationErrors(t *testing.T) {
dir := t.TempDir()
invalidCA := filepath.Join(dir, "invalid.pem")
if err := os.WriteFile(invalidCA, []byte("not a certificate"), 0600); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
values map[string]any
wantErr string
}{
{name: "missing-address", wantErr: "--address is required"},
{name: "legacy-bypass", values: map[string]any{"tls_skip_verify": true}, wantErr: "no longer supported"},
{name: "both-options", values: map[string]any{"tls_skip_verify": true, "insecure": true}, wantErr: "no longer supported"},
{name: "remote-plaintext", values: map[string]any{"address": "192.0.2.1:50000", "insecure": true}, wantErr: "literal loopback"},
{name: "ca-with-plaintext", values: map[string]any{"address": "127.0.0.1:50000", "insecure": true, "tls_ca_cert": invalidCA}, wantErr: "cannot be used"},
{name: "missing-ca", values: map[string]any{"address": "dataplane.example:50000", "tls_ca_cert": filepath.Join(dir, "missing.pem")}, wantErr: "read --tls_ca_cert"},
{name: "invalid-ca", values: map[string]any{"address": "dataplane.example:50000", "tls_ca_cert": invalidCA}, wantErr: "no valid PEM certificates"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
t.Cleanup(viper.Reset)
for key, value := range test.values {
viper.Set(key, value)
}
conn, err := Dial()
if conn != nil {
conn.Close()
t.Fatal("invalid configuration created a connection")
}
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("Dial() error = %v; want %q", err, test.wantErr)
}
})
}
}
5 changes: 3 additions & 2 deletions dataplane/luciusctl/luciusctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ func New() *cobra.Command {
Short: "lucius queries info from a running instance of lucius.",
}
cmd.PersistentFlags().StringP("address", "a", "", "Address of dataplane server")
cmd.PersistentFlags().Bool("insecure", false, "Use plaintext to dial server")
cmd.PersistentFlags().Bool("tls_skip_verify", false, "Use TLS without cert verification")
cmd.PersistentFlags().Bool("insecure", false, "Use plaintext only for a literal loopback IP and port")
cmd.PersistentFlags().Bool("tls_skip_verify", false, "Unsupported: use --tls_ca_cert instead")
cmd.PersistentFlags().String("tls_ca_cert", "", "PEM CA bundle to trust in addition to system certificate roots")

cobra.OnInitialize(func() { viper.BindPFlags(cmd.Flags()) })
viper.BindPFlags(cmd.Flags())
Expand Down
Loading
Loading