Skip to content
Merged
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
20 changes: 18 additions & 2 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ const (
UNBOUNDED = "unbounded"
)

// Client capabilities, advertised in ConfigRequest.Capabilities, let the server
// enable optional behavior per-client — capability negotiation instead of
// version sniffing.
const (
// CapabilityNonSelectableOutbounds: the client honors
// ConfigResponse.NonSelectableOutbounds (merges those outbounds into its box
// config but keeps them out of the proxy-selection groups). The server gates
// infrastructure outbounds like the proxyless download_detour on this, so an
// older client can't surface one as a selectable proxy and route traffic
// through it.
CapabilityNonSelectableOutbounds = "non_selectable_outbounds"
)

type ServerLocation struct {
Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
Expand Down Expand Up @@ -120,6 +133,9 @@ type ConfigRequest struct {
Backend string `json:"backend,omitempty"`
Locale string `json:"locale,omitempty"`
Protocols []string `json:"protocols,omitempty"`
MetricsOptedIn bool `json:"metrics_opted_in,omitempty"`
Version string `json:"version,omitempty"`
// Capabilities advertises optional client behaviors the server can gate on
// (see the Capability* consts), e.g. honoring NonSelectableOutbounds.
Capabilities []string `json:"capabilities,omitempty"`
MetricsOptedIn bool `json:"metrics_opted_in,omitempty"`
Version string `json:"version,omitempty"`
}
22 changes: 22 additions & 0 deletions types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,28 @@ func TestNonSelectableOutboundsRoundTrip(t *testing.T) {
}
}

func TestCapabilitiesRoundTrip(t *testing.T) {
original := ConfigRequest{
Capabilities: []string{CapabilityNonSelectableOutbounds},
}

data, err := json.Marshal(original)
assert.NoError(t, err)
assert.Contains(t, string(data), `"capabilities"`)

var deserialized ConfigRequest
err = json.Unmarshal(data, &deserialized)
assert.NoError(t, err)
assert.Equal(t, []string{CapabilityNonSelectableOutbounds}, deserialized.Capabilities)

// Both nil and a non-nil empty slice should be omitted from JSON.
for _, empty := range []ConfigRequest{{}, {Capabilities: []string{}}} {
data, err = json.Marshal(empty)
assert.NoError(t, err)
assert.NotContains(t, string(data), `"capabilities"`)
}
}

func TestConfigResponseDefaultValues(t *testing.T) {
resp := ConfigResponse{}
if len(resp.Servers) != 0 {
Expand Down