diff --git a/internal/confparser/confparser.go b/internal/confparser/confparser.go index 76102a3..5ccd1cb 100644 --- a/internal/confparser/confparser.go +++ b/internal/confparser/confparser.go @@ -3,6 +3,7 @@ package confparser import ( "fmt" "net" + "net/url" "reflect" "slices" "strconv" @@ -372,6 +373,10 @@ func (f *FieldConfig) validateField() error { if _, err := idna.Lookup.ToASCII(val); err != nil { return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val) } + case "proxy": + if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" { + return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val) + } default: return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType) } diff --git a/internal/network/config.go b/internal/network/config.go index 74ddf19..2d61295 100644 --- a/internal/network/config.go +++ b/internal/network/config.go @@ -3,6 +3,8 @@ package network import ( "fmt" "net" + "net/http" + "net/url" "time" "github.com/guregu/null/v6" @@ -32,8 +34,9 @@ type IPv6StaticConfig struct { DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"` } type NetworkConfig struct { - Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` - Domain null.String `json:"domain,omitempty" validate_type:"hostname"` + Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` + HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"` + Domain null.String `json:"domain,omitempty" validate_type:"hostname"` IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"` IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"` @@ -69,6 +72,18 @@ func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions { return listenOptions } + +func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) { + return func(*http.Request) (*url.URL, error) { + if s.HTTPProxy.String == "" { + return nil, nil + } else { + proxyUrl, _ := url.Parse(s.HTTPProxy.String) + return proxyUrl, nil + } + } +} + func (s *NetworkInterfaceState) GetHostname() string { hostname := ToValidHostname(s.config.Hostname.String) diff --git a/internal/timesync/http.go b/internal/timesync/http.go index 3a51463..68f9442 100644 --- a/internal/timesync/http.go +++ b/internal/timesync/http.go @@ -5,6 +5,7 @@ import ( "errors" "math/rand" "net/http" + "net/url" "strconv" "time" ) @@ -57,6 +58,7 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now ctx, url, timeout, + t.networkConfig.GetTransportProxyFunc(), ) duration := time.Since(startTime) @@ -111,10 +113,16 @@ func queryHttpTime( ctx context.Context, url string, timeout time.Duration, + proxyFunc func(*http.Request) (*url.URL, error), ) (now *time.Time, response *http.Response, err error) { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = proxyFunc + client := http.Client{ - Timeout: timeout, + Transport: transport, + Timeout: timeout, } + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err diff --git a/ota.go b/ota.go index cf97cc0..75d94a4 100644 --- a/ota.go +++ b/ota.go @@ -89,7 +89,14 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := http.DefaultClient.Do(req) + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = config.NetworkConfig.GetTransportProxyFunc() + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error sending request: %w", err) } @@ -135,6 +142,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress client := http.Client{ Timeout: 10 * time.Minute, Transport: &http.Transport{ + Proxy: config.NetworkConfig.GetTransportProxyFunc(), TLSHandshakeTimeout: 30 * time.Second, TLSClientConfig: &tls.Config{ RootCAs: rootcerts.ServerCertPool(), diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 52ef89d..28ce99a 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -730,6 +730,7 @@ export type TimeSyncMode = export interface NetworkSettings { hostname: string; domain: string; + http_proxy: string; ipv4_mode: IPv4Mode; ipv6_mode: IPv6Mode; lldp_mode: LLDPMode; diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 0905db5..6fcd588 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -34,6 +34,7 @@ dayjs.extend(relativeTime); const defaultNetworkSettings: NetworkSettings = { hostname: "", + http_proxy: "", domain: "", ipv4_mode: "unknown", ipv6_mode: "unknown", @@ -185,6 +186,10 @@ export default function SettingsNetworkRoute() { setNetworkSettings({ ...networkSettings, hostname: value }); }; + const handleProxyChange = (value: string) => { + setNetworkSettings({ ...networkSettings, http_proxy: value }); + }; + const handleDomainChange = (value: string) => { setNetworkSettings({ ...networkSettings, domain: value }); }; @@ -253,6 +258,26 @@ export default function SettingsNetworkRoute() { +