Example #1
0
        /// <summary>
        /// Validates the instance.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public void Validate(TrafficValidationContext context)
        {
            if (ConnectSeconds < 0.0)
            {
                context.Error($"Load balancer timeout [{nameof(ConnectSeconds)}={ConnectSeconds}] is not valid.");
            }

            if (ClientSeconds < 0.0)
            {
                context.Error($"Load balancer timeout [{nameof(ClientSeconds)}={ClientSeconds}] is not valid.");
            }

            if (HttpKeepAliveSeconds < 0.0)
            {
                context.Error($"Load balancer timeout [{nameof(HttpKeepAliveSeconds)}={HttpKeepAliveSeconds}] is not valid.");
            }

            if (ServerSeconds < 0.0)
            {
                context.Error($"Load balancer timeout [{nameof(ServerSeconds)}={ServerSeconds}] is not valid.");
            }

            if (CheckSeconds <= 0.0)
            {
                context.Error($"Load balancer timeout [{nameof(CheckSeconds)}={CheckSeconds}] is not positive.");
            }
        }
Example #2
0
        /// <summary>
        /// Validates the instance.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public void Validate(TrafficValidationContext context)
        {
            if (string.IsNullOrWhiteSpace(Name))
            {
                context.Error($"Load balancer resolver [{nameof(Name)}] cannot be null or empty.");
            }

            if (NameServers == null || NameServers.Count == 0)
            {
                context.Error($"Load balancer resolver [{nameof(NameServers)}] at least one name server must be specified.");
            }

            if (ResolveRetries < 0)
            {
                context.Error($"Load balancer resolver [{nameof(ResolveRetries)}={ResolveRetries}] is not valid.");
            }

            if (RetrySeconds <= 0.0)
            {
                context.Error($"Load balancer resolver [{nameof(RetrySeconds)}={RetrySeconds}] is not valid.");
            }

            if (HoldSeconds < 0.0)
            {
                context.Error($"Load balancer resolver [{nameof(HoldSeconds)}={HoldSeconds}] is not valid.");
            }
        }
Example #3
0
        /// <summary>
        /// Validates the instance.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public void Validate(TrafficValidationContext context)
        {
            if (string.IsNullOrWhiteSpace(Name))
            {
                context.Error("Load balancer nameserver name cannot be null or empty.");
            }

            var isValid = false;

            if (!string.IsNullOrWhiteSpace(Endpoint))
            {
                var colonPos = Endpoint.LastIndexOf(':');

                if (colonPos >= 0)
                {
                    var addressPart = Endpoint.Substring(0, colonPos);
                    var portPart    = Endpoint.Substring(colonPos + 1);

                    ushort port;

                    isValid = IPAddress.TryParse(addressPart, out var address) && ushort.TryParse(portPart, out port);
                }

                if (!isValid)
                {
                    context.Error($"[{nameof(TrafficNameserver)}.{nameof(Name)}={Endpoint}] is not valid.");
                }
            }
        }
Example #4
0
        /// <summary>
        /// Validates the header.
        /// </summary>
        /// <param name="context">The validation context.</param>
        /// <param name="rule">The parent rule.</param>
        public void Validate(TrafficValidationContext context, TrafficRule rule)
        {
            if (string.IsNullOrWhiteSpace(Name))
            {
                context.Error($"Rule [{rule.Name}] specifies a NULL or empty [{nameof(TrafficCheckHeader)}.{nameof(TrafficCheckHeader.Name)}].");
            }

            foreach (var ch in Name)
            {
                if (char.IsLetterOrDigit(ch) || ch == '-' || ch == '_')
                {
                    continue;
                }

                context.Error($"Rule [{rule.Name}] specifies a [{nameof(TrafficCheckHeader)}.{nameof(TrafficCheckHeader.Name)}] with the invalid character [{ch}].");
                break;
            }

            if (string.IsNullOrWhiteSpace(Value))
            {
                context.Error($"Rule [{rule.Name}] specifies a NULL [{nameof(TrafficCheckHeader)}.{nameof(TrafficCheckHeader.Value)}].");
            }

            // $todo(jeff.lill):
            //
            // We could try to validate the [Value] property too (e.g. to ensure that it doesn't include "\r\n") but
            // that could be overly restrictive if I'm not careful.  I'm going to leave this be for now.
        }
Example #5
0
        /// <summary>
        /// Validates the frontend.
        /// </summary>
        /// <param name="context">The validation context.</param>
        /// <param name="rule">The parent rule.</param>
        public void Validate(TrafficValidationContext context, TrafficRule rule)
        {
            // Verify [MaxConnections]

            if (MaxConnections < 0)
            {
                context.Error($"Rule [{rule.Name}] specifies invalid [{nameof(MaxConnections)}={MaxConnections}].");
            }
        }
Example #6
0
        /// <summary>
        /// Validates the instance.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public void Validate(TrafficValidationContext context)
        {
            Timeouts              = Timeouts ?? new TrafficTimeouts();
            Resolvers             = Resolvers ?? new List <TrafficResolver>();
            BridgeTargetAddresses = BridgeTargetAddresses ?? new List <IPAddress>();

            if (!Resolvers.Exists(r => r.Name == "docker"))
            {
                Resolvers.Add(
                    new TrafficResolver()
                {
                    Name        = "docker",
                    NameServers = new List <TrafficNameserver>()
                    {
                        new TrafficNameserver()
                        {
                            Name     = "docker0",
                            Endpoint = HiveConst.DockerDnsEndpoint
                        }
                    }
                });
            }

            if (!NetHelper.IsValidPort(ProxyPorts.PortRange.FirstPort) ||
                !NetHelper.IsValidPort(ProxyPorts.PortRange.LastPort) ||
                ProxyPorts.PortRange.LastPort <= ProxyPorts.PortRange.FirstPort + 1)
            {
                context.Error($"Load balancer port block [{ProxyPorts.PortRange.FirstPort}-{ProxyPorts.PortRange.LastPort}] range is not valid.");
            }

            if (MaxConnections <= 0)
            {
                context.Error($"Load balancer settings [{nameof(MaxConnections)}={MaxConnections}] is not positive.");
            }

            Timeouts.Validate(context);

            if (!Resolvers.Exists(r => r.Name == "docker"))
            {
                context.Error($"Load balancer settings [{nameof(Resolvers)}] must include a [docker] definition.");
            }

            foreach (var resolver in Resolvers)
            {
                resolver.Validate(context);
            }

            if (BridgeTargetCount < 0)
            {
                context.Error($"Load balancer settings [{nameof(BridgeTargetCount)}={BridgeTargetCount}] cannot be negative.");
            }

            if (BridgeTargetCount == 0 && BridgeTargetAddresses.Count == 0)
            {
                context.Error($"Load balancer settings no bridge targets are specified.");
            }
        }
Example #7
0
        /// <summary>
        /// Validates the frontend.
        /// </summary>
        /// <param name="context">The validation context.</param>
        /// <param name="rule">The parent rule.</param>
        public void Validate(TrafficValidationContext context, TrafficTcpRule rule)
        {
            base.Validate(context, rule);

            if (PublicPort > 0 && !NetHelper.IsValidPort(PublicPort))
            {
                context.Error($"Load balancer [{nameof(PublicPort)}={PublicPort}] is not a valid network port.");
            }

            if (!context.Settings.ProxyPorts.IsValidTcpPort(ProxyPort))
            {
                context.Error($"Rule [{rule.Name}] assigns [{nameof(ProxyPort)}={ProxyPort}] which is outside the range of valid frontend TCP ports for this traffic manager [{context.Settings.ProxyPorts}].");
            }
        }
Example #8
0
        /// <summary>
        /// Validates the rule.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public override void Validate(TrafficValidationContext context)
        {
            base.Validate(context);

            Frontends = Frontends ?? new List <TrafficTcpFrontend>();
            Backends  = Backends ?? new List <TrafficTcpBackend>();

            if (Frontends.Count == 0)
            {
                context.Error($"Rule [{Name}] has does not define a frontend.");
            }

            if (Backends.Count == 0)
            {
                context.Error($"Rule [{Name}] has does not define a backend.");
            }

            foreach (var frontend in Frontends)
            {
                frontend.Validate(context, this);
            }

            foreach (var backend in Backends)
            {
                backend.Validate(context, this);
            }

            // Verify that the ports are unique for each frontend and that none of these TCP
            // target one of the reserved HTTP/HTTPS proxy ports.

            var frontendMap = new HashSet <int>();

            foreach (var frontend in Frontends)
            {
                var key = frontend.ProxyPort;

                if (frontendMap.Contains(key))
                {
                    context.Error($"TCP rule [{Name}] includes two or more frontends that map to port [{key}].");
                }

                if (frontend.ProxyPort == HiveHostPorts.ProxyPublicHttp || frontend.ProxyPort == HiveHostPorts.ProxyPublicHttps ||
                    frontend.ProxyPort == HiveHostPorts.ProxyPublicHttp || frontend.ProxyPort == HiveHostPorts.ProxyPublicHttps)
                {
                    context.Error($"Rule [{Name}] has a TCP frontend with [{nameof(frontend.ProxyPort)}={frontend.ProxyPort}] that is incorrectly mapped to a reserved HTTP/HTTPS port.");
                }

                frontendMap.Add(key);
            }
        }
Example #9
0
        /// <summary>
        /// Validates the instance.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public virtual void Validate(TrafficValidationContext context)
        {
            Timeouts = Timeouts ?? new TrafficTimeouts();

            if (string.IsNullOrEmpty(Name))
            {
                context.Error($"Load balancer rule name is required.");
            }

            if (!HiveDefinition.NameRegex.IsMatch(Name))
            {
                context.Error($"Load balancer rule name [{nameof(Name)}={Name}] is not valid.");
            }

            if (context.ValidateResolvers)
            {
                if (!string.IsNullOrWhiteSpace(Resolver) &&
                    context.Settings != null &&
                    context.Settings.Resolvers.Count(r => string.Equals(Resolver, r.Name, StringComparison.OrdinalIgnoreCase)) == 0)
                {
                    context.Error($"Load balancer resolver [{nameof(Resolver)}={Resolver}] does not exist.");
                }
            }

            Resolver     = Resolver ?? defaultResolverName;
            CheckHeaders = CheckHeaders ?? new List <TrafficCheckHeader>();

            foreach (var checkHeader in CheckHeaders)
            {
                checkHeader.Validate(context, this);
            }

            if (UseHttpCheckMode)
            {
                if (string.IsNullOrEmpty(CheckUri) || !Uri.TryCreate(CheckUri, UriKind.RelativeOrAbsolute, out var uri))
                {
                    context.Error($"Rule [{nameof(Name)}] has invalid [{nameof(CheckUri)}={CheckUri}].");
                }
            }

            if (CheckSeconds < 0.0)
            {
                CheckSeconds = 5.0;
            }

            Timeouts.Validate(context);
        }
Example #10
0
        /// <summary>
        /// Validates the item.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public void Validate(TrafficValidationContext context)
        {
            if (string.IsNullOrEmpty(Uri))
            {
                context.Error($"[{nameof(TrafficWarmTarget)}.{nameof(Uri)}] cannot be NULL or empty.");
            }

            if (!System.Uri.TryCreate(Uri, UriKind.Absolute, out var uri))
            {
                context.Error($"[{nameof(TrafficWarmTarget)}.{nameof(Uri)}={Uri}] is not a valid fully qualified URI.");
            }

            if (UpdateSeconds <= 0)
            {
                UpdateSeconds = defaultUpdateSeconds;
            }
        }
Example #11
0
        /// <summary>
        /// Validates the settings.
        /// </summary>
        /// <param name="context">The validation context.</param>
        /// <param name="rule">The parent rule.</param>
        public void Validate(TrafficValidationContext context, TrafficHttpRule rule)
        {
            if (DnsTTL < 1)
            {
                context.Error($"[{nameof(TrafficHttpCache)}.{nameof(DnsTTL)}={DnsTTL}] cannot be less than 1 second.");
            }

            WarmTargets = WarmTargets ?? new List <TrafficWarmTarget>();

            // Verify that each warm target has valid properties and that they
            // all match at one of the rule frontends.

            foreach (var target in WarmTargets)
            {
                target.Validate(context);

                if (rule.GetFrontendForWarmTarget(target) == null)
                {
                    context.Error($"Rule [{rule.Name}] includes the [{target.Uri}] cache warming target which cannot be mapped to a rule frontend.");
                }
            }
        }
Example #12
0
        /// <summary>
        /// Validates the backend.
        /// </summary>
        /// <param name="context">The validation context.</param>
        /// <param name="ruleName">The parent rule name.</param>
        public virtual void Validate(TrafficValidationContext context, string ruleName)
        {
            if (!string.IsNullOrEmpty(Name) && !HiveDefinition.IsValidName(Name))
            {
                context.Error($"Rule [{ruleName}] has backend server with invalid [{nameof(Name)}={Name}].");
            }

            if (!string.IsNullOrEmpty(Group))
            {
                if (!HiveDefinition.IsValidName(Group))
                {
                    context.Error($"Rule [{ruleName}] has backend with [{nameof(Group)}={Group}] which is not a valid group name.");
                }

                if (GroupLimit < 0)
                {
                    context.Error($"Rule [{ruleName}] has backend with [{nameof(GroupLimit)}={GroupLimit}] which may not be less than zero.");
                }
            }
            else
            {
                if (string.IsNullOrEmpty(Server) ||
                    (!IPAddress.TryParse(Server, out var address) && !HiveDefinition.DnsHostRegex.IsMatch(Server)))
                {
                    context.Error($"Rule [{ruleName}] has backend server [{Server}] which is not valid.  A DNS name or IP address was expected.");
                }
            }

            if (!NetHelper.IsValidPort(Port))
            {
                context.Error($"Rule [{ruleName}] has backend server with invalid [{nameof(Port)}={Port}] which is outside the range of valid TCP ports.");
            }

            if (MaxConnections < 0)
            {
                context.Error($"Rule [{ruleName}] has backend server with invalid [{nameof(MaxConnections)}={MaxConnections}].");
            }
        }
Example #13
0
        /// <summary>
        /// Adds or updates a traffic manager rule.
        /// </summary>
        /// <param name="rule">The rule definition.</param>
        /// <param name="deferUpdate">
        /// <para>
        /// Optionally defers expicitly notifying the <b>neon-proxy-manager</b> of the
        /// change until <see cref="Update()"/> is called or the <b>neon-proxy-manager</b>
        /// performs the periodic check for changes (which defaults to 60 seconds).  You
        /// may consider passing <paramref name="deferUpdate"/><c>=true</c> when you are
        /// modifying a multiple rules at the same time to avoid making the proxy manager
        /// and proxy instances handle each rule change individually.
        /// </para>
        /// <para>
        /// Instead, you could pass <paramref name="deferUpdate"/><c>=true</c> for all of
        /// the rule changes and then call <see cref="Update()"/> afterwards.
        /// </para>
        /// </param>
        /// <returns>
        /// <c>true</c> if it existed and was updated, <b>false</b>
        /// if the traffic manager rule didn't already exist and was added.
        /// </returns>
        /// <exception cref="HiveDefinitionException">Thrown if the rule is not valid.</exception>
        public bool SetRule(TrafficRule rule, bool deferUpdate = false)
        {
            Covenant.Requires <ArgumentNullException>(rule != null);
            Covenant.Requires <ArgumentNullException>(HiveDefinition.IsValidName(rule.Name));

            if (!IsPublic)
            {
                // Ensure that the [PublicPort] is disabled for non-public rules
                // just to be absolutely sure that these endpoints are not exposed
                // to the Internet for cloud deployments and to avoid operators
                // being freaked out if they see a non-zero port here.

                var httpRule = rule as TrafficHttpRule;

                if (httpRule != null)
                {
                    foreach (var frontEnd in httpRule.Frontends)
                    {
                        frontEnd.PublicPort = 0;
                    }
                }
                else
                {
                    var tcpRule = rule as TrafficTcpRule;

                    if (tcpRule != null)
                    {
                        foreach (var frontEnd in tcpRule.Frontends)
                        {
                            frontEnd.PublicPort = 0;
                        }
                    }
                }
            }

            // $todo(jeff.lill):
            //
            // We're going to minimially ensure that the rule is valid.  It would
            // be better to do full server side validation.

            var context = new TrafficValidationContext(Name, GetSettings())
            {
                ValidateCertificates = false,   // Disable this because we didn't download the certs.
                ValidateResolvers    = false
            };

            rule.Validate(context);
            context.ThrowIfErrors();

            // Publish the rule.

            var ruleKey = GetProxyRuleKey(rule.Name);
            var update  = hive.Consul.Client.KV.Exists(ruleKey).Result;

            // Load the full proxy definition and hive certificates, add/replace
            // the rule being set and then verify that the rule is OK.

            var proxyDefinition = GetDefinition();
            var certificates    = hive.Certificate.GetAll();

            proxyDefinition.Rules[rule.Name] = rule;
            proxyDefinition.Validate(certificates);

            var validationContext = proxyDefinition.Validate(certificates);

            validationContext.ThrowIfErrors();

            // Save the rule to the hive and signal that the
            // load balancers need to be updated.

            hive.Consul.Client.KV.PutObject(ruleKey, rule, Formatting.Indented).Wait();

            if (!deferUpdate)
            {
                Update();
            }

            return(update);
        }
Example #14
0
        /// <summary>
        /// Validates the traffic manager definition.
        /// </summary>
        /// <param name="certificates">The dictionary of hive certificates keyed by name.</param>
        /// <returns>The <see cref="TrafficValidationContext"/>.</returns>
        public TrafficValidationContext Validate(Dictionary <string, TlsCertificate> certificates)
        {
            Covenant.Requires <ArgumentNullException>(certificates != null);

            var context = new TrafficValidationContext(Name, Settings, certificates);

            // Validate the existing settings and rules.

            Settings.Validate(context);

            foreach (var rule in Rules.Values)
            {
                rule.Validate(context);
            }

            // Verify that there are no existing frontend port/host conflicts:
            //
            //      * HTTP rules can share ports but hostnames must be unique.
            //      * HTTP rules on the same port cannot mix TLS and non-TLS.
            //      * Only one TCP port per rule is allowed.

            var httpMap       = new Dictionary <string, TrafficHttpRule>(StringComparer.OrdinalIgnoreCase);
            var httpPortToTls = new Dictionary <int, bool>();
            var tcpMap        = new Dictionary <int, TrafficTcpRule>();

            // Scan HTTP rules.

            foreach (var rule in Rules.Values.Where(r => r.Mode == TrafficMode.Http).OrderBy(r => r.Name.ToLowerInvariant()))
            {
                var httpRule = (TrafficHttpRule)rule;

                foreach (var frontend in httpRule.Frontends)
                {
                    var key = $"{frontend.Host}:{frontend.ProxyPort}";

                    if (!string.IsNullOrEmpty(frontend.PathPrefix))
                    {
                        key += frontend.PathPrefix;
                    }

                    if (httpMap.ContainsKey(key))
                    {
                        context.Error($"HTTP rule [{httpRule.Name}] has a frontend on [{key}] that conflicts with rule [{httpMap[key].Name}].");
                        continue;
                    }

                    httpMap.Add(key, httpRule);

                    if (!httpPortToTls.TryGetValue(frontend.ProxyPort, out var isTls))
                    {
                        isTls = frontend.Tls;
                        httpPortToTls.Add(frontend.ProxyPort, isTls);
                    }

                    if (isTls != frontend.Tls)
                    {
                        if (frontend.Tls)
                        {
                            context.Error($"HTTP rule [{httpRule.Name}] has a TLS frontend on port [{frontend.ProxyPort}] that conflicts with non-TLS frontends on this port.");
                        }
                        else
                        {
                            context.Error($"HTTP rule [{httpRule.Name}] has a non-TLS frontend on port [{frontend.ProxyPort}] that conflicts with TLS frontends on this port.");
                        }
                    }
                }
            }

            // Scan the TCP rules.

            foreach (var rule in Rules.Values.Where(r => r.Mode == TrafficMode.Tcp).OrderBy(r => r.Name.ToLowerInvariant()))
            {
                var tcpRule = (TrafficTcpRule)rule;

                foreach (var frontend in tcpRule.Frontends)
                {
                    var port = frontend.ProxyPort;

                    if (port == Settings.DefaultHttpPort || port == Settings.DefaultHttpsPort)
                    {
                        context.Error($"TCP rule [{tcpRule.Name}] has a frontend on [{port}] that conflicts the default traffic manager HTTP or HTTPS port.");
                    }

                    if (httpPortToTls.ContainsKey(port))
                    {
                        context.Error($"TCP rule [{tcpRule.Name}] has a frontend on [{port}] that conflicts with one or more HTTP traffic manager frontends on the same port.");
                    }

                    if (tcpMap.ContainsKey(port))
                    {
                        context.Error($"TCP rule [{tcpRule.Name}] has a frontend on [{port}] that conflicts with rule [{tcpMap[port].Name}].");
                    }

                    tcpMap.Add(port, tcpRule);
                }
            }

            return(context);
        }
Example #15
0
 /// <summary>
 /// Validates the backend.
 /// </summary>
 /// <param name="context">The validation context.</param>
 /// <param name="rule">The parent rule.</param>
 public void Validate(TrafficValidationContext context, TrafficHttpRule rule)
 {
     base.Validate(context, rule.Name);
 }
Example #16
0
        /// <summary>
        /// Validates the frontend.
        /// </summary>
        /// <param name="context">The validation context.</param>
        /// <param name="rule">The parent rule.</param>
        public void Validate(TrafficValidationContext context, TrafficHttpRule rule)
        {
            base.Validate(context, rule);

            if (rule.Frontends.Count > 1 ||
                !string.IsNullOrEmpty(CertName) ||
                ProxyPort == 0 ||
                ProxyPort == HiveHostPorts.ProxyPublicHttp || ProxyPort == HiveHostPorts.ProxyPublicHttps ||
                ProxyPort == HiveHostPorts.ProxyPrivateHttp || ProxyPort == HiveHostPorts.ProxyPrivateHttps)
            {
                // The hostname is required so verify it.

                if (string.IsNullOrEmpty(Host))
                {
                    context.Error($"Rule [{rule.Name}] has a frontend without a [{nameof(Host)}] specified.  HTTP rules targeting the default traffic manager HTTP/S ports, with more than one frontend, or secured by TLS requires frontend hostnames.");
                }
                else if (!HiveDefinition.DnsHostRegex.IsMatch(Host))
                {
                    context.Error($"Rule [{rule.Name}] defines the invalid hostname [{Host}].");
                }
            }
            else
            {
                // The hostname is not required but verify it if one is specified.

                if (!string.IsNullOrEmpty(Host) && !HiveDefinition.DnsHostRegex.IsMatch(Host))
                {
                    context.Error($"Rule [{rule.Name}] defines the invalid hostname [{Host}].");
                }
            }

            if (!string.IsNullOrEmpty(PathPrefix))
            {
                if (!PathPrefix.StartsWith("/"))
                {
                    context.Error($"Rule [{rule.Name}] references has [{nameof(PathPrefix)}={PathPrefix}] that does not begin with a forward slash.");
                }
                else
                {
                    if (!PathPrefix.EndsWith("/"))
                    {
                        PathPrefix += "/";
                    }

                    if (!Uri.TryCreate(PathPrefix, UriKind.Relative, out Uri uri))
                    {
                        context.Error($"Rule [{rule.Name}] references has [{nameof(PathPrefix)}={PathPrefix}] that is not a valid relative URI.");
                    }
                }
            }

            if (CertName != null && context.ValidateCertificates)
            {
                TlsCertificate certificate;

                if (!context.Certificates.TryGetValue(CertName, out certificate))
                {
                    context.Error($"Rule [{rule.Name}] references certificate [{CertName}] that does not exist or could not be loaded.");
                }
                else
                {
                    if (!certificate.IsValidHost(Host))
                    {
                        context.Error($"Rule [{rule.Name}] references certificate [{CertName}] which does not cover host [{Host}].");
                    }

                    if (!certificate.IsValidDate(DateTime.UtcNow))
                    {
                        context.Error($"Rule [{rule.Name}] references certificate [{CertName}] which expired on [{certificate.ValidUntil}].");
                    }
                }
            }

            if (ProxyPort != 0)
            {
                if (!context.Settings.ProxyPorts.IsValidPort(ProxyPort))
                {
                    context.Error($"Rule [{rule.Name}] assigns [{nameof(ProxyPort)}={ProxyPort}] which is outside the range of valid frontend ports for this traffic manager [{context.Settings.ProxyPorts}].");
                }
            }
            else
            {
                if (CertName == null)
                {
                    ProxyPort = context.Settings.DefaultHttpPort;
                }
                else
                {
                    ProxyPort = context.Settings.DefaultHttpsPort;
                }
            }

            if (PublicPort == -1)
            {
                if (CertName == null)
                {
                    PublicPort = NetworkPorts.HTTP;
                }
                else
                {
                    PublicPort = NetworkPorts.HTTPS;
                }
            }

            if (PublicPort > 0 && !NetHelper.IsValidPort(PublicPort))
            {
                context.Error($"Load balancer [{nameof(PublicPort)}={PublicPort}] is not a valid network port.");
            }

            if (RedirectTo != null)
            {
                // Strip off the path/query part of the URI, if necessary.

                if (RedirectTo.PathAndQuery != "/")
                {
                    RedirectTo = new Uri($"{RedirectTo.Scheme}://{RedirectTo.Host}:{RedirectTo.Port}/");
                }
            }
        }
Example #17
0
        /// <summary>
        /// Validates the rule.
        /// </summary>
        /// <param name="context">The validation context.</param>
        public override void Validate(TrafficValidationContext context)
        {
            base.Validate(context);

            Frontends = Frontends ?? new List <TrafficHttpFrontend>();
            Backends  = Backends ?? new List <TrafficHttpBackend>();

            if (Frontends.Count == 0)
            {
                context.Error($"Rule [{Name}] has does not define a frontend.");
            }

            if (!string.IsNullOrEmpty(CheckUri))
            {
                if (!Uri.TryCreate(CheckUri, UriKind.Relative, out var uri))
                {
                    context.Error($"Rule [{Name}] has invalid [{nameof(CheckUri)}={CheckUri}].");
                }
            }

            if (string.IsNullOrEmpty(CheckMethod) || CheckMethod.IndexOfAny(new char[] { ' ', '\r', '\n', '\t' }) != -1)
            {
                context.Error($"Rule [{Name}] has invalid [{nameof(CheckMethod)}={CheckMethod}].");
            }

            if (string.IsNullOrEmpty(CheckVersion))
            {
                CheckVersion = "1.0";
            }

            var regex = new Regex(@"^\d+\.\d+$");

            if (!regex.Match(CheckVersion).Success)
            {
                context.Error($"Rule [{Name}] has invalid [{nameof(CheckVersion)}={CheckVersion}].");
            }

            if (!string.IsNullOrEmpty(CheckHost) && !HiveDefinition.DnsHostRegex.Match(CheckHost).Success)
            {
                context.Error($"Rule [{Name}] has invalid [{nameof(CheckHost)}={CheckHost}].");
            }

            if (!string.IsNullOrEmpty(CheckExpect))
            {
                var error = $"Rule [{Name}] has invalid [{nameof(CheckExpect)}={CheckExpect}].";
                var value = CheckExpect.Trim();

                if (value.StartsWith("! "))
                {
                    value = value.Substring(2).Trim();
                }

                var pos = value.IndexOf(' ');

                if (pos == -1)
                {
                    context.Error(error + "  Expected: <match> <pattern>");
                }
                else
                {
                    var match   = value.Substring(0, pos);
                    var pattern = value.Substring(pos).Trim();

                    if (pattern.Replace("\\ ", string.Empty).IndexOf(' ') != -1)
                    {
                        context.Error(error + $"  Pattern [{pattern}] includes unescaped spaces.");
                    }

                    switch (match)
                    {
                    case "status":
                    case "string":

                        break;

                    case "rstatus":
                    case "rstring":

                        try
                        {
                            new Regex(pattern);
                        }
                        catch (Exception e)
                        {
                            context.Error(error + $"  Pattern regex [{pattern}] parsing error: {e.Message}.");
                        }
                        break;

                    default:

                        context.Error(error + "  Invalid [match], expected one of: status, rstatus, string, rstring");
                        break;
                    }
                }
            }

            foreach (var frontend in Frontends)
            {
                frontend.Validate(context, this);
            }

            foreach (var backend in Backends)
            {
                backend.Validate(context, this);
            }

            // Verify that the port/host combinations are unique for each frontend.

            var frontendMap = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

            foreach (var frontend in Frontends)
            {
                if (string.IsNullOrEmpty(frontend.PathPrefix))
                {
                    var key = $"{frontend.Host}:{frontend.ProxyPort}";

                    if (frontendMap.Contains(key))
                    {
                        context.Error($"HTTP rule [{Name}] includes two or more frontends that map to [{key}].");
                    }

                    frontendMap.Add(key);
                }
            }

            foreach (var frontend in Frontends)
            {
                if (!string.IsNullOrEmpty(frontend.PathPrefix))
                {
                    var key = $"{frontend.Host}:{frontend.ProxyPort}{frontend.PathPrefix}";

                    if (frontendMap.Contains($"{frontend.Host}:{frontend.ProxyPort}") ||    // Ensure there's no *all* path frontend
                        frontendMap.Contains(key))
                    {
                        context.Error($"HTTP rule [{Name}] includes two or more frontends that map to [{key}].");
                    }

                    frontendMap.Add(key);
                }
            }

            if (Cache != null && Cache.Enabled)
            {
                Cache.Validate(context, this);

                // The Varnish open source release doesn't support TLS backends.  This requires
                // Varnish Plus (of course) which is very expensive.

                foreach (TrafficHttpBackend backend in Backends)
                {
                    if (backend.Tls)
                    {
                        context.Error($"HTTP rule [{Name}] cannot support caching because one or more backends required TLS.");
                        break;
                    }
                }

                // Varnish doesn't support comparing health probe status codes with a regex
                // like HAProxy does.  We're going to enforce having CheckExpect set to
                // something like "status 200".

                var statusFields = CheckExpect.Split(' ');

                if (statusFields.Length != 2 || statusFields[0] != "status" ||
                    !int.TryParse(statusFields[1], out var statusCode) ||
                    statusCode < 100 || 600 <= statusCode)
                {
                    context.Error($"HTTP rule [{Name}] cannot support caching because [{nameof(CheckExpect)}={CheckExpect}] doesn't specify a fixed status code like [status 200].  Varnish-Cache does not support verifying health probe status codes as regular expressions like HAProxy can.");
                }

                // $todo(jeff.lill):
                //
                // We need to enforce some restrictions due to Varnish limitations
                // described here:
                //
                //      https://github.com/jefflill/NeonForge/issues/379
                //
                // It would be nice to revisit this in the future.

                // Ensure that:
                //
                //      * If one backend has a hostname then it must be the only backend.
                //      * IP address and hostname backends cannot be mixed.

                if (Backends.Count > 1)
                {
                    var hasHostname  = false;
                    var hasIPAddress = false;

                    foreach (var backend in Backends)
                    {
                        if (IPAddress.TryParse(backend.Server, out var address))
                        {
                            hasIPAddress = true;
                        }
                        else
                        {
                            hasHostname = true;
                        }
                    }

                    if (hasIPAddress)
                    {
                        context.Error($"HTTP rule [{Name}] has multiple backends reachable via hostname which is not supported.  You may define only a single backend that requires a DNS lookup.");
                    }
                    else if (hasIPAddress && hasHostname)
                    {
                        context.Error($"HTTP rule [{Name}] has backends reachable via IP address and hostname which is not supported.  You cannot mix backends with IP address and hostnames in the same rule.");
                    }
                }

                // Ensure that all cache warming targets have schemes, hostnames, ports that
                // match a rule frontend, and that HTTP rules don't map to reserved HTTPS ports
                // and HTTPS rules don't map to reserved HTTP ports.

                foreach (var frontend in Frontends)
                {
                    if (frontend.Tls)
                    {
                        if (frontend.ProxyPort == HiveHostPorts.ProxyPublicHttp || frontend.ProxyPort == HiveHostPorts.ProxyPrivateHttp)
                        {
                            context.Error($"Rule [{Name}] has an HTTPS frontend with [{nameof(frontend.ProxyPort)}={frontend.ProxyPort}] that is incorrectly mapped to a reserved HTTP port.");
                        }
                    }
                    else
                    {
                        if (frontend.ProxyPort == HiveHostPorts.ProxyPublicHttps || frontend.ProxyPort == HiveHostPorts.ProxyPrivateHttps)
                        {
                            context.Error($"Rule [{Name}] has an HTTP frontend with [{nameof(frontend.ProxyPort)}={frontend.ProxyPort}] that is incorrectly mapped to a reserved HTTPS port.");
                        }
                    }
                }

                // Ensure that all cache warming targets have schemes, hostnames, and ports that
                // match a rule frontend.

                foreach (var warmTarget in Cache.WarmTargets)
                {
                    var uri = new Uri(warmTarget.Uri);
                    var tls = uri.Scheme.Equals("https", StringComparison.InvariantCultureIgnoreCase);

                    if (Frontends.IsEmpty(fe => fe.Tls == tls && fe.Host.Equals(uri.Host, StringComparison.InvariantCultureIgnoreCase) && fe.ProxyPort == uri.Port))
                    {
                        context.Error($"Cache warm target [{uri}] does not match one of the [{Name}] traffic manager frontends.");
                    }
                }
            }
        }