Updates tailscale/corp#15043 Signed-off-by: James Tucker <james@tailscale.com>main
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…
Reference in new issue