From 1b76627ba314323a430dc66868268c1c991a3e0f Mon Sep 17 00:00:00 2001 From: Andrew Pearson Date: Tue, 7 Jan 2025 03:58:25 +0000 Subject: [PATCH 1/4] Adding changes --- .devcontainer/devcontainer.json | 22 +++++++++++++++ config.go | 7 +++++ doh.go | 50 ++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..34955282 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/config.go b/config.go index 3f9b2f8c..93faaa00 100644 --- a/config.go +++ b/config.go @@ -243,6 +243,13 @@ type UpstreamConfig struct { // The caller should not access this field directly. // Use UpstreamSendClientInfo instead. SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"` + + // ClientId is the client info that will be sent to upstream. + ClientId string `mapstructure:"client_id" toml:"client_id,omitempty" validate:"omitempty, oneof=host mac"` + + // How to transmit client info to upstream. + ClientIdType string `mapstructure:"client_info_type" toml:"client_info_type,omitempty" validate:"omitempty,oneof=subdomain path header"` + // The caller should not access this field directly. // Use IsDiscoverable instead. Discoverable *bool `mapstructure:"discoverable" toml:"discoverable"` diff --git a/doh.go b/doh.go index d7029955..45af3264 100644 --- a/doh.go +++ b/doh.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "regexp" "runtime" "strings" "sync" @@ -94,6 +95,17 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro query.Add("dns", enc) endpoint := *r.endpoint + + if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil { + switch r.uc.ClientIdType { + case "subdomain": + endpoint.Host = subdomainFromClientInfo(r.uc, ci) + "." + endpoint.Host + + case "path": + endpoint.Path = endpoint.Path + pathFromClientInfo(r.uc, ci) + } + } + endpoint.RawQuery = query.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) if err != nil { @@ -147,7 +159,7 @@ func addHeader(ctx context.Context, req *http.Request, uc *UpstreamConfig) { if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil { printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != "" switch { - case uc.IsControlD(): + case uc.IsControlD() || uc.ClientIdType == "header": dohHeader = newControlDHeaders(ci) case uc.isNextDNS(): dohHeader = newNextDNSHeaders(ci) @@ -202,3 +214,39 @@ func newNextDNSHeaders(ci *ClientInfo) http.Header { } return header } + +func subdomainFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { + switch uc.ClientId { + case "mac": + return strings.ReplaceAll(ci.Mac, ":", "") + case "host": + return subdomainFromHostname(ci.Hostname) + } + return "" // TODO; fix this +} + +func pathFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { + switch uc.ClientId { + case "mac": + return "/" + url.PathEscape(ci.Mac) + case "host": + return "/" + url.PathEscape(ci.Hostname) + } + return "" // TODO; fix this +} + +func subdomainFromHostname(hostname string) string { + // Define a regular expression to match allowed characters + re := regexp.MustCompile(`[^a-zA-Z0-9-]`) + + // Remove chars not allowed in subdomain + subdomain := re.ReplaceAllString(hostname, "") + + // Replace spaces with -- + subdomain = strings.ReplaceAll(subdomain, " ", "--") + + // Trim leading and trailing hyphens + subdomain = strings.Trim(subdomain, "-") + + return subdomain +} From 90dc34d870574b108a489ef7e03ca5be5c1cbe1c Mon Sep 17 00:00:00 2001 From: Andrew Pearson Date: Tue, 7 Jan 2025 06:00:03 +0000 Subject: [PATCH 2/4] Mac using - instead of : --- config.go | 4 ++-- doh.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index 93faaa00..d6b8cb92 100644 --- a/config.go +++ b/config.go @@ -245,10 +245,10 @@ type UpstreamConfig struct { SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"` // ClientId is the client info that will be sent to upstream. - ClientId string `mapstructure:"client_id" toml:"client_id,omitempty" validate:"omitempty, oneof=host mac"` + ClientId string `mapstructure:"client_id" toml:"client_id,omitempty"` // How to transmit client info to upstream. - ClientIdType string `mapstructure:"client_info_type" toml:"client_info_type,omitempty" validate:"omitempty,oneof=subdomain path header"` + ClientIdType string `mapstructure:"client_id_type" toml:"client_id_type,omitempty"` // The caller should not access this field directly. // Use IsDiscoverable instead. diff --git a/doh.go b/doh.go index 45af3264..1dc95440 100644 --- a/doh.go +++ b/doh.go @@ -218,7 +218,7 @@ func newNextDNSHeaders(ci *ClientInfo) http.Header { func subdomainFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { switch uc.ClientId { case "mac": - return strings.ReplaceAll(ci.Mac, ":", "") + return strings.ReplaceAll(ci.Mac, ":", "-") case "host": return subdomainFromHostname(ci.Hostname) } @@ -228,7 +228,7 @@ func subdomainFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { func pathFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { switch uc.ClientId { case "mac": - return "/" + url.PathEscape(ci.Mac) + return "/" + strings.ReplaceAll(ci.Mac, ":", "-") case "host": return "/" + url.PathEscape(ci.Hostname) } From 01f6b1e77de4b96c4bdcd56432c74e3981793520 Mon Sep 17 00:00:00 2001 From: Andrew Pearson Date: Wed, 8 Jan 2025 04:16:31 +0000 Subject: [PATCH 3/4] It's working! --- config.go | 5 +---- doh.go | 36 +++++++++++++++++++----------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/config.go b/config.go index d6b8cb92..a2d27091 100644 --- a/config.go +++ b/config.go @@ -244,10 +244,7 @@ type UpstreamConfig struct { // Use UpstreamSendClientInfo instead. SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"` - // ClientId is the client info that will be sent to upstream. - ClientId string `mapstructure:"client_id" toml:"client_id,omitempty"` - - // How to transmit client info to upstream. + // How to transmit client info to upstream. (e.g. default:Headers, Subdomain, Path) ClientIdType string `mapstructure:"client_id_type" toml:"client_id_type,omitempty"` // The caller should not access this field directly. diff --git a/doh.go b/doh.go index 1dc95440..0ac63b90 100644 --- a/doh.go +++ b/doh.go @@ -2,7 +2,9 @@ package ctrld import ( "context" + "crypto/sha256" "encoding/base64" + "encoding/hex" "errors" "fmt" "io" @@ -99,10 +101,10 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil { switch r.uc.ClientIdType { case "subdomain": - endpoint.Host = subdomainFromClientInfo(r.uc, ci) + "." + endpoint.Host + endpoint.Host = clientIdFromClientInfo(r.uc, ci) + "." + endpoint.Host case "path": - endpoint.Path = endpoint.Path + pathFromClientInfo(r.uc, ci) + endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/" + clientIdFromClientInfo(r.uc, ci) } } @@ -159,7 +161,7 @@ func addHeader(ctx context.Context, req *http.Request, uc *UpstreamConfig) { if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil { printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != "" switch { - case uc.IsControlD() || uc.ClientIdType == "header": + case uc.IsControlD() || uc.ClientIdType == "" || uc.ClientIdType == "headers": dohHeader = newControlDHeaders(ci) case uc.isNextDNS(): dohHeader = newNextDNSHeaders(ci) @@ -215,27 +217,27 @@ func newNextDNSHeaders(ci *ClientInfo) http.Header { return header } -func subdomainFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { - switch uc.ClientId { +func clientIdFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { + switch ci.ClientIDPref { case "mac": - return strings.ReplaceAll(ci.Mac, ":", "-") + return clientIdFromMac(ci.Mac) case "host": - return subdomainFromHostname(ci.Hostname) + return clientIdFromHostname(ci.Hostname) } - return "" // TODO; fix this + return hashHostnameAndMac(ci.Hostname, ci.Mac) } -func pathFromClientInfo(uc *UpstreamConfig, ci *ClientInfo) string { - switch uc.ClientId { - case "mac": - return "/" + strings.ReplaceAll(ci.Mac, ":", "-") - case "host": - return "/" + url.PathEscape(ci.Hostname) - } - return "" // TODO; fix this +func hashHostnameAndMac(hostname, mac string) string { + h := sha256.New() + h.Write([]byte(hostname + mac)) + return hex.EncodeToString(h.Sum(nil)) +} + +func clientIdFromMac(mac string) string { + return strings.ReplaceAll(mac, ":", "-") } -func subdomainFromHostname(hostname string) string { +func clientIdFromHostname(hostname string) string { // Define a regular expression to match allowed characters re := regexp.MustCompile(`[^a-zA-Z0-9-]`) From 4e539b71f1e8ba0f9262054208ee0cd4654632f4 Mon Sep 17 00:00:00 2001 From: Andrew Pearson Date: Wed, 8 Jan 2025 07:12:15 +0000 Subject: [PATCH 4/4] Removing dev container --- .devcontainer/devcontainer.json | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 34955282..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,22 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/go -{ - "name": "Go", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm" - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "go version", - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -}