appctype: introduce a configuration schema for app connectors

Updates tailscale/corp#15043

Signed-off-by: James Tucker <james@tailscale.com>
pull/9785/head
James Tucker 8 months ago committed by James Tucker
parent 37c646d9d3
commit ce0830837d

@ -0,0 +1,59 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package appcfg contains an experimental configuration structure for
// "tailscale.com/app-connector" capmap extensions.
package appctype
import (
"net/netip"
"tailscale.com/tailcfg"
)
// ConfigID is an opaque identifier for a configuration.
type ConfigID string
// AppConnectorConfig is the configuration structure for an application
// connection proxy service.
type AppConnectorConfig struct {
// DNAT is a map of destination NAT configurations.
DNAT map[ConfigID]DNATConfig `json:",omitempty"`
// SNIProxy is a map of SNI proxy configurations.
SNIProxy map[ConfigID]SNIProxyConfig `json:",omitempty"`
// AdvertiseRoutes indicates that the node should advertise routes for each
// of the addresses in service configuration address lists. If false, the
// routes have already been advertised.
AdvertiseRoutes bool `json:",omitempty"`
}
// DNATConfig is the configuration structure for a destination NAT service, also
// known as a "port forward" or "port proxy".
type DNATConfig struct {
// Addrs is a list of addresses to listen on.
Addrs []netip.Addr `json:",omitempty"`
// To is a list of destination addresses to forward traffic to. It should
// only contain one domain, or a list of IP addresses.
To []string `json:",omitempty"`
// IP is a list of IP specifications to forward. If omitted, all protocols are
// forwarded. IP specifications are of the form "tcp/80", "udp/53", etc.
IP []tailcfg.ProtoPortRange `json:",omitempty"`
}
// SNIPRoxyConfig is the configuration structure for an SNI proxy service,
// forwarding TLS connections based on the hostname field in SNI.
type SNIProxyConfig struct {
// Addrs is a list of addresses to listen on.
Addrs []netip.Addr `json:",omitempty"`
// IP is a list of IP specifications to forward. If omitted, all protocols are
// forwarded. IP specifications are of the form "tcp/80", "udp/53", etc.
IP []tailcfg.ProtoPortRange `json:",omitempty"`
// AllowedDomains is a list of domains that are allowed to be proxied. If
// the domain starts with a `.` that means any subdomain of the suffix.
AllowedDomains []string `json:",omitempty"`
}

@ -0,0 +1,78 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package appctype
import (
"encoding/json"
"net/netip"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/tailcfg"
"tailscale.com/util/must"
)
var golden = `{
"dnat": {
"opaqueid1": {
"addrs": ["100.64.0.1", "fd7a:115c:a1e0::1"],
"to": ["example.org"],
"ip": ["*"]
}
},
"sniProxy": {
"opaqueid2": {
"addrs": ["::"],
"ip": ["tcp:443"],
"allowedDomains": ["*"]
}
},
"advertiseRoutes": true
}`
func TestGolden(t *testing.T) {
wantDNAT := map[ConfigID]DNATConfig{"opaqueid1": {
Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
To: []string{"example.org"},
IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
}}
wantSNI := map[ConfigID]SNIProxyConfig{"opaqueid2": {
Addrs: []netip.Addr{netip.MustParseAddr("::")},
IP: []tailcfg.ProtoPortRange{{Proto: 6, Ports: tailcfg.PortRange{First: 443, Last: 443}}},
AllowedDomains: []string{"*"},
}}
var config AppConnectorConfig
if err := json.NewDecoder(strings.NewReader(golden)).Decode(&config); err != nil {
t.Fatalf("failed to decode golden config: %v", err)
}
if !config.AdvertiseRoutes {
t.Fatalf("expected AdvertiseRoutes to be true, got false")
}
assertEqual(t, "DNAT", config.DNAT, wantDNAT)
assertEqual(t, "SNI", config.SNIProxy, wantSNI)
}
func TestRoundTrip(t *testing.T) {
var config AppConnectorConfig
must.Do(json.NewDecoder(strings.NewReader(golden)).Decode(&config))
b := must.Get(json.Marshal(config))
var config2 AppConnectorConfig
must.Do(json.Unmarshal(b, &config2))
assertEqual(t, "DNAT", config.DNAT, config2.DNAT)
}
func assertEqual(t *testing.T, name string, a, b any) {
var addrComparer = cmp.Comparer(func(a, b netip.Addr) bool {
return a.Compare(b) == 0
})
t.Helper()
if diff := cmp.Diff(a, b, addrComparer); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
}
Loading…
Cancel
Save