istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/istio-iptables/pkg/capture/run.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package capture 15 16 import ( 17 "fmt" 18 "net" 19 "net/netip" 20 "os" 21 "strings" 22 23 "github.com/vishvananda/netlink" 24 25 "istio.io/istio/pkg/log" 26 "istio.io/istio/tools/istio-iptables/pkg/builder" 27 "istio.io/istio/tools/istio-iptables/pkg/config" 28 "istio.io/istio/tools/istio-iptables/pkg/constants" 29 dep "istio.io/istio/tools/istio-iptables/pkg/dependencies" 30 iptableslog "istio.io/istio/tools/istio-iptables/pkg/log" 31 ) 32 33 type Ops int 34 35 const ( 36 // AppendOps performs append operations of rules 37 AppendOps Ops = iota 38 // DeleteOps performs delete operations of rules 39 DeleteOps 40 41 // In TPROXY mode, mark the packet from envoy outbound to app by podIP, 42 // this is to prevent it being intercepted to envoy inbound listener. 43 outboundMark = "1338" 44 ) 45 46 var opsToString = map[Ops]string{ 47 AppendOps: "-A", 48 DeleteOps: "-D", 49 } 50 51 type IptablesConfigurator struct { 52 ruleBuilder *builder.IptablesRuleBuilder 53 // TODO(abhide): Fix dep.Dependencies with better interface 54 ext dep.Dependencies 55 cfg *config.Config 56 } 57 58 func NewIptablesConfigurator(cfg *config.Config, ext dep.Dependencies) *IptablesConfigurator { 59 return &IptablesConfigurator{ 60 ruleBuilder: builder.NewIptablesRuleBuilder(cfg), 61 ext: ext, 62 cfg: cfg, 63 } 64 } 65 66 type NetworkRange struct { 67 IsWildcard bool 68 CIDRs []netip.Prefix 69 HasLoopBackIP bool 70 } 71 72 func split(s string) []string { 73 return config.Split(s) 74 } 75 76 func (cfg *IptablesConfigurator) separateV4V6(cidrList string) (NetworkRange, NetworkRange, error) { 77 if cidrList == "*" { 78 return NetworkRange{IsWildcard: true}, NetworkRange{IsWildcard: true}, nil 79 } 80 ipv6Ranges := NetworkRange{} 81 ipv4Ranges := NetworkRange{} 82 for _, ipRange := range split(cidrList) { 83 ipp, err := netip.ParsePrefix(ipRange) 84 if err != nil { 85 _, err = fmt.Fprintf(os.Stderr, "Ignoring error for bug compatibility with istio-iptables: %s\n", err.Error()) 86 if err != nil { 87 return ipv4Ranges, ipv6Ranges, err 88 } 89 continue 90 } 91 if ipp.Addr().Is4() { 92 ipv4Ranges.CIDRs = append(ipv4Ranges.CIDRs, ipp) 93 if ipp.Addr().IsLoopback() { 94 ipv4Ranges.HasLoopBackIP = true 95 } 96 } else { 97 ipv6Ranges.CIDRs = append(ipv6Ranges.CIDRs, ipp) 98 if ipp.Addr().IsLoopback() { 99 ipv6Ranges.HasLoopBackIP = true 100 } 101 } 102 } 103 return ipv4Ranges, ipv6Ranges, nil 104 } 105 106 func (cfg *IptablesConfigurator) logConfig() { 107 // Dump out our environment for debugging purposes. 108 var b strings.Builder 109 b.WriteString(fmt.Sprintf("ENVOY_PORT=%s\n", os.Getenv("ENVOY_PORT"))) 110 b.WriteString(fmt.Sprintf("INBOUND_CAPTURE_PORT=%s\n", os.Getenv("INBOUND_CAPTURE_PORT"))) 111 b.WriteString(fmt.Sprintf("ISTIO_INBOUND_INTERCEPTION_MODE=%s\n", os.Getenv("ISTIO_INBOUND_INTERCEPTION_MODE"))) 112 b.WriteString(fmt.Sprintf("ISTIO_INBOUND_TPROXY_ROUTE_TABLE=%s\n", os.Getenv("ISTIO_INBOUND_TPROXY_ROUTE_TABLE"))) 113 b.WriteString(fmt.Sprintf("ISTIO_INBOUND_PORTS=%s\n", os.Getenv("ISTIO_INBOUND_PORTS"))) 114 b.WriteString(fmt.Sprintf("ISTIO_OUTBOUND_PORTS=%s\n", os.Getenv("ISTIO_OUTBOUND_PORTS"))) 115 b.WriteString(fmt.Sprintf("ISTIO_LOCAL_EXCLUDE_PORTS=%s\n", os.Getenv("ISTIO_LOCAL_EXCLUDE_PORTS"))) 116 b.WriteString(fmt.Sprintf("ISTIO_EXCLUDE_INTERFACES=%s\n", os.Getenv("ISTIO_EXCLUDE_INTERFACES"))) 117 b.WriteString(fmt.Sprintf("ISTIO_SERVICE_CIDR=%s\n", os.Getenv("ISTIO_SERVICE_CIDR"))) 118 b.WriteString(fmt.Sprintf("ISTIO_SERVICE_EXCLUDE_CIDR=%s\n", os.Getenv("ISTIO_SERVICE_EXCLUDE_CIDR"))) 119 b.WriteString(fmt.Sprintf("ISTIO_META_DNS_CAPTURE=%s\n", os.Getenv("ISTIO_META_DNS_CAPTURE"))) 120 b.WriteString(fmt.Sprintf("INVALID_DROP=%s\n", os.Getenv("INVALID_DROP"))) 121 log.Infof("Istio iptables environment:\n%s", b.String()) 122 cfg.cfg.Print() 123 } 124 125 func (cfg *IptablesConfigurator) handleInboundPortsInclude() { 126 // Handling of inbound ports. Traffic will be redirected to Envoy, which will process and forward 127 // to the local service. If not set, no inbound port will be intercepted by istio iptablesOrFail. 128 var table string 129 if cfg.cfg.InboundPortsInclude != "" { 130 if cfg.cfg.InboundInterceptionMode == constants.TPROXY { 131 // When using TPROXY, create a new chain for routing all inbound traffic to 132 // Envoy. Any packet entering this chain gets marked with the ${INBOUND_TPROXY_MARK} mark, 133 // so that they get routed to the loopback interface in order to get redirected to Envoy. 134 // In the ISTIOINBOUND chain, '-j ISTIODIVERT' reroutes to the loopback 135 // interface. 136 // Mark all inbound packets. 137 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIODIVERT, constants.MANGLE, "-j", constants.MARK, "--set-mark", 138 cfg.cfg.InboundTProxyMark) 139 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIODIVERT, constants.MANGLE, "-j", constants.ACCEPT) 140 141 // Create a new chain for redirecting inbound traffic to the common Envoy 142 // port. 143 // In the ISTIOINBOUND chain, '-j RETURN' bypasses Envoy and 144 // '-j ISTIOTPROXY' redirects to Envoy. 145 cfg.ruleBuilder.AppendVersionedRule(cfg.cfg.HostIPv4LoopbackCidr, "::1/128", iptableslog.UndefinedCommand, 146 constants.ISTIOTPROXY, constants.MANGLE, "!", "-d", constants.IPVersionSpecific, 147 "-p", constants.TCP, "-j", constants.TPROXY, 148 "--tproxy-mark", cfg.cfg.InboundTProxyMark+"/0xffffffff", "--on-port", cfg.cfg.InboundCapturePort) 149 table = constants.MANGLE 150 } else { 151 table = constants.NAT 152 } 153 cfg.ruleBuilder.AppendRule(iptableslog.JumpInbound, constants.PREROUTING, table, "-p", constants.TCP, 154 "-j", constants.ISTIOINBOUND) 155 156 if cfg.cfg.InboundPortsInclude == "*" { 157 // Apply any user-specified port exclusions. 158 if cfg.cfg.InboundPortsExclude != "" { 159 for _, port := range split(cfg.cfg.InboundPortsExclude) { 160 cfg.ruleBuilder.AppendRule(iptableslog.ExcludeInboundPort, constants.ISTIOINBOUND, table, "-p", constants.TCP, 161 "--dport", port, "-j", constants.RETURN) 162 } 163 } 164 // Redirect remaining inbound traffic to Envoy. 165 if cfg.cfg.InboundInterceptionMode == constants.TPROXY { 166 // If an inbound packet belongs to an established socket, route it to the 167 // loopback interface. 168 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.MANGLE, "-p", constants.TCP, 169 "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", constants.ISTIODIVERT) 170 // Otherwise, it's a new connection. Redirect it using TPROXY. 171 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.MANGLE, "-p", constants.TCP, 172 "-j", constants.ISTIOTPROXY) 173 } else { 174 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.NAT, "-p", constants.TCP, 175 "-j", constants.ISTIOINREDIRECT) 176 } 177 } else { 178 // User has specified a non-empty list of ports to be redirected to Envoy. 179 for _, port := range split(cfg.cfg.InboundPortsInclude) { 180 if cfg.cfg.InboundInterceptionMode == constants.TPROXY { 181 cfg.ruleBuilder.AppendRule(iptableslog.IncludeInboundPort, constants.ISTIOINBOUND, constants.MANGLE, "-p", constants.TCP, 182 "--dport", port, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", constants.ISTIODIVERT) 183 cfg.ruleBuilder.AppendRule(iptableslog.IncludeInboundPort, 184 constants.ISTIOINBOUND, constants.MANGLE, "-p", constants.TCP, "--dport", port, "-j", constants.ISTIOTPROXY) 185 } else { 186 cfg.ruleBuilder.AppendRule(iptableslog.IncludeInboundPort, 187 constants.ISTIOINBOUND, constants.NAT, "-p", constants.TCP, "--dport", port, "-j", constants.ISTIOINREDIRECT) 188 } 189 } 190 } 191 } 192 } 193 194 func (cfg *IptablesConfigurator) handleOutboundIncludeRules( 195 rangeInclude NetworkRange, 196 appendRule func(command iptableslog.Command, chain string, table string, params ...string) *builder.IptablesRuleBuilder, 197 insert func(command iptableslog.Command, chain string, table string, position int, params ...string) *builder.IptablesRuleBuilder, 198 ) { 199 // Apply outbound IP inclusions. 200 if rangeInclude.IsWildcard { 201 // Wildcard specified. Redirect all remaining outbound traffic to Envoy. 202 appendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-j", constants.ISTIOREDIRECT) 203 for _, internalInterface := range split(cfg.cfg.KubeVirtInterfaces) { 204 insert(iptableslog.KubevirtCommand, 205 constants.PREROUTING, constants.NAT, 1, "-i", internalInterface, "-j", constants.ISTIOREDIRECT) 206 } 207 } else if len(rangeInclude.CIDRs) > 0 { 208 // User has specified a non-empty list of cidrs to be redirected to Envoy. 209 for _, cidr := range rangeInclude.CIDRs { 210 for _, internalInterface := range split(cfg.cfg.KubeVirtInterfaces) { 211 insert(iptableslog.KubevirtCommand, constants.PREROUTING, constants.NAT, 1, "-i", internalInterface, 212 "-d", cidr.String(), "-j", constants.ISTIOREDIRECT) 213 } 214 appendRule(iptableslog.UndefinedCommand, 215 constants.ISTIOOUTPUT, constants.NAT, "-d", cidr.String(), "-j", constants.ISTIOREDIRECT) 216 } 217 // All other traffic is not redirected. 218 appendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-j", constants.RETURN) 219 } 220 } 221 222 func (cfg *IptablesConfigurator) shortCircuitKubeInternalInterface() { 223 for _, internalInterface := range split(cfg.cfg.KubeVirtInterfaces) { 224 cfg.ruleBuilder.InsertRule(iptableslog.KubevirtCommand, constants.PREROUTING, constants.NAT, 1, "-i", internalInterface, "-j", constants.RETURN) 225 } 226 } 227 228 func (cfg *IptablesConfigurator) shortCircuitExcludeInterfaces() { 229 for _, excludeInterface := range split(cfg.cfg.ExcludeInterfaces) { 230 cfg.ruleBuilder.AppendRule( 231 iptableslog.ExcludeInterfaceCommand, constants.PREROUTING, constants.NAT, "-i", excludeInterface, "-j", constants.RETURN) 232 cfg.ruleBuilder.AppendRule(iptableslog.ExcludeInterfaceCommand, constants.OUTPUT, constants.NAT, "-o", excludeInterface, "-j", constants.RETURN) 233 } 234 if cfg.cfg.InboundInterceptionMode == constants.TPROXY { 235 for _, excludeInterface := range split(cfg.cfg.ExcludeInterfaces) { 236 237 cfg.ruleBuilder.AppendRule( 238 iptableslog.ExcludeInterfaceCommand, constants.PREROUTING, constants.MANGLE, "-i", excludeInterface, "-j", constants.RETURN) 239 cfg.ruleBuilder.AppendRule(iptableslog.ExcludeInterfaceCommand, constants.OUTPUT, constants.MANGLE, "-o", excludeInterface, "-j", constants.RETURN) 240 } 241 } 242 } 243 244 func ignoreExists(err error) error { 245 if err == nil { 246 return nil 247 } 248 if strings.Contains(strings.ToLower(err.Error()), "file exists") { 249 return nil 250 } 251 return err 252 } 253 254 // configureIPv6Addresses sets up a new IP address on local interface. This is used as the source IP 255 // for inbound traffic to distinguish traffic we want to capture vs traffic we do not. This is needed 256 // for IPv6 but not IPv4, as IPv4 defaults to `netmask 255.0.0.0`, which allows binding to addresses 257 // in the 127.x.y.z range, while IPv6 defaults to `prefixlen 128` which allows binding only to ::1. 258 // Equivalent to `ip -6 addr add "::6/128" dev lo` 259 func configureIPv6Addresses(cfg *config.Config) error { 260 if !cfg.EnableIPv6 { 261 return nil 262 } 263 link, err := netlink.LinkByName("lo") 264 if err != nil { 265 return fmt.Errorf("failed to find 'lo' link: %v", err) 266 } 267 // Setup a new IP address on local interface. This is used as the source IP for inbound traffic 268 // to distinguish traffic we want to capture vs traffic we do not. 269 // Equivalent to `ip -6 addr add "::6/128" dev lo` 270 address := &net.IPNet{IP: net.ParseIP("::6"), Mask: net.CIDRMask(128, 128)} 271 addr := &netlink.Addr{IPNet: address} 272 273 err = netlink.AddrAdd(link, addr) 274 if ignoreExists(err) != nil { 275 return fmt.Errorf("failed to add IPv6 inbound address: %v", err) 276 } 277 log.Infof("Added ::6 address") 278 return nil 279 } 280 281 func (cfg *IptablesConfigurator) Run() error { 282 iptVer, err := cfg.ext.DetectIptablesVersion(false) 283 if err != nil { 284 return err 285 } 286 287 ipt6Ver, err := cfg.ext.DetectIptablesVersion(true) 288 if err != nil { 289 return err 290 } 291 292 defer func() { 293 // Best effort since we don't know if the commands exist 294 _ = cfg.ext.Run(constants.IPTablesSave, &iptVer, nil) 295 if cfg.cfg.EnableIPv6 { 296 _ = cfg.ext.Run(constants.IPTablesSave, &ipt6Ver, nil) 297 } 298 }() 299 300 // Since OUTBOUND_IP_RANGES_EXCLUDE could carry ipv4 and ipv6 ranges 301 // need to split them in different arrays one for ipv4 and one for ipv6 302 // in order to not to fail 303 ipv4RangesExclude, ipv6RangesExclude, err := cfg.separateV4V6(cfg.cfg.OutboundIPRangesExclude) 304 if err != nil { 305 return err 306 } 307 if ipv4RangesExclude.IsWildcard { 308 return fmt.Errorf("invalid value for OUTBOUND_IP_RANGES_EXCLUDE") 309 } 310 // FixMe: Do we need similar check for ipv6RangesExclude as well ?? 311 312 ipv4RangesInclude, ipv6RangesInclude, err := cfg.separateV4V6(cfg.cfg.OutboundIPRangesInclude) 313 if err != nil { 314 return err 315 } 316 317 redirectDNS := cfg.cfg.RedirectDNS 318 cfg.logConfig() 319 320 cfg.shortCircuitExcludeInterfaces() 321 322 // Do not capture internal interface. 323 cfg.shortCircuitKubeInternalInterface() 324 325 // Create a rule for invalid drop in PREROUTING chain in mangle table, so the iptables will drop the out of window packets instead of reset connection . 326 dropInvalid := cfg.cfg.DropInvalid 327 if dropInvalid { 328 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.PREROUTING, constants.MANGLE, "-m", "conntrack", "--ctstate", 329 "INVALID", "-j", constants.DROP) 330 } 331 332 // Create a new chain for to hit tunnel port directly. Envoy will be listening on port acting as VPN tunnel. 333 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.NAT, "-p", constants.TCP, "--dport", 334 cfg.cfg.InboundTunnelPort, "-j", constants.RETURN) 335 336 // Create a new chain for redirecting outbound traffic to the common Envoy port. 337 // In both chains, '-j RETURN' bypasses Envoy and '-j ISTIOREDIRECT' 338 // redirects to Envoy. 339 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, 340 constants.ISTIOREDIRECT, constants.NAT, "-p", constants.TCP, "-j", constants.REDIRECT, "--to-ports", cfg.cfg.ProxyPort) 341 342 // Use this chain also for redirecting inbound traffic to the common Envoy port 343 // when not using TPROXY. 344 345 cfg.ruleBuilder.AppendRule(iptableslog.InboundCapture, constants.ISTIOINREDIRECT, constants.NAT, "-p", constants.TCP, "-j", constants.REDIRECT, 346 "--to-ports", cfg.cfg.InboundCapturePort) 347 348 cfg.handleInboundPortsInclude() 349 350 // TODO: change the default behavior to not intercept any output - user may use http_proxy or another 351 // iptablesOrFail wrapper (like ufw). Current default is similar with 0.1 352 // Jump to the ISTIOOUTPUT chain from OUTPUT chain for all tcp traffic 353 cfg.ruleBuilder.AppendRule(iptableslog.JumpOutbound, constants.OUTPUT, constants.NAT, "-p", constants.TCP, "-j", constants.ISTIOOUTPUT) 354 // Apply port based exclusions. Must be applied before connections back to self are redirected. 355 if cfg.cfg.OutboundPortsExclude != "" { 356 for _, port := range split(cfg.cfg.OutboundPortsExclude) { 357 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-p", constants.TCP, 358 "--dport", port, "-j", constants.RETURN) 359 } 360 } 361 362 // 127.0.0.6/::7 is bind connect from inbound passthrough cluster 363 cfg.ruleBuilder.AppendVersionedRule("127.0.0.6/32", "::6/128", iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 364 "-o", "lo", "-s", constants.IPVersionSpecific, "-j", constants.RETURN) 365 366 for _, uid := range split(cfg.cfg.ProxyUID) { 367 // Redirect app calls back to itself via Envoy when using the service VIP 368 // e.g. appN => Envoy (client) => Envoy (server) => appN. 369 // nolint: lll 370 if redirectDNS { 371 // When DNS is enabled, we skip this for port 53. This ensures we do not have: 372 // app => istio-agent => Envoy inbound => dns server 373 // Instead, we just have: 374 // app => istio-agent => dns server 375 cfg.ruleBuilder.AppendVersionedRule(cfg.cfg.HostIPv4LoopbackCidr, "::1/128", iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 376 "-o", "lo", 377 "!", "-d", constants.IPVersionSpecific, 378 "-p", "tcp", 379 "-m", "multiport", 380 "!", "--dports", "53,"+cfg.cfg.InboundTunnelPort, 381 "-m", "owner", "--uid-owner", uid, "-j", constants.ISTIOINREDIRECT) 382 } else { 383 cfg.ruleBuilder.AppendVersionedRule(cfg.cfg.HostIPv4LoopbackCidr, "::1/128", iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 384 "-o", "lo", 385 "!", "-d", constants.IPVersionSpecific, 386 "-p", "tcp", 387 "!", "--dport", cfg.cfg.InboundTunnelPort, 388 "-m", "owner", "--uid-owner", uid, "-j", constants.ISTIOINREDIRECT) 389 } 390 // Do not redirect app calls to back itself via Envoy when using the endpoint address 391 // e.g. appN => appN by lo 392 // If loopback explicitly set via OutboundIPRangesInclude, then don't return. 393 if !ipv4RangesInclude.HasLoopBackIP && !ipv6RangesInclude.HasLoopBackIP { 394 if redirectDNS { 395 // Users may have a DNS server that is on localhost. In these cases, applications may 396 // send TCP traffic to the DNS server that we actually *do* want to intercept. To 397 // handle this case, we exclude port 53 from this rule. Note: We cannot just move the 398 // port 53 redirection rule further up the list, as we will want to avoid capturing 399 // DNS requests from the proxy UID/GID 400 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-o", "lo", "-p", "tcp", 401 "!", "--dport", "53", 402 "-m", "owner", "!", "--uid-owner", uid, "-j", constants.RETURN) 403 } else { 404 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 405 "-o", "lo", "-m", "owner", "!", "--uid-owner", uid, "-j", constants.RETURN) 406 } 407 } 408 409 // Avoid infinite loops. Don't redirect Envoy traffic directly back to 410 // Envoy for non-loopback traffic. 411 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 412 "-m", "owner", "--uid-owner", uid, "-j", constants.RETURN) 413 } 414 415 for _, gid := range split(cfg.cfg.ProxyGID) { 416 // Redirect app calls back to itself via Envoy when using the service VIP 417 // e.g. appN => Envoy (client) => Envoy (server) => appN. 418 cfg.ruleBuilder.AppendVersionedRule(cfg.cfg.HostIPv4LoopbackCidr, "::1/128", iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 419 "-o", "lo", 420 "!", "-d", constants.IPVersionSpecific, 421 "-p", "tcp", 422 "!", "--dport", cfg.cfg.InboundTunnelPort, 423 "-m", "owner", "--gid-owner", gid, "-j", constants.ISTIOINREDIRECT) 424 425 // Do not redirect app calls to back itself via Envoy when using the endpoint address 426 // e.g. appN => appN by lo 427 // If loopback explicitly set via OutboundIPRangesInclude, then don't return. 428 if !ipv4RangesInclude.HasLoopBackIP && !ipv6RangesInclude.HasLoopBackIP { 429 if redirectDNS { 430 // Users may have a DNS server that is on localhost. In these cases, applications may 431 // send TCP traffic to the DNS server that we actually *do* want to intercept. To 432 // handle this case, we exclude port 53 from this rule. Note: We cannot just move the 433 // port 53 redirection rule further up the list, as we will want to avoid capturing 434 // DNS requests from the proxy UID/GID 435 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 436 "-o", "lo", "-p", "tcp", 437 "!", "--dport", "53", 438 "-m", "owner", "!", "--gid-owner", gid, "-j", constants.RETURN) 439 } else { 440 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 441 "-o", "lo", "-m", "owner", "!", "--gid-owner", gid, "-j", constants.RETURN) 442 } 443 } 444 445 // Avoid infinite loops. Don't redirect Envoy traffic directly back to 446 // Envoy for non-loopback traffic. 447 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-m", "owner", "--gid-owner", gid, "-j", constants.RETURN) 448 } 449 450 ownerGroupsFilter := config.ParseInterceptFilter(cfg.cfg.OwnerGroupsInclude, cfg.cfg.OwnerGroupsExclude) 451 452 cfg.handleCaptureByOwnerGroup(ownerGroupsFilter) 453 454 if redirectDNS { 455 if cfg.cfg.CaptureAllDNS { 456 // Redirect all TCP dns traffic on port 53 to the agent on port 15053 457 // This will be useful for the CNI case where pod DNS server address cannot be decided. 458 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, 459 constants.ISTIOOUTPUT, constants.NAT, 460 "-p", constants.TCP, 461 "--dport", "53", 462 "-j", constants.REDIRECT, 463 "--to-ports", constants.IstioAgentDNSListenerPort) 464 } else { 465 for _, s := range cfg.cfg.DNSServersV4 { 466 // redirect all TCP dns traffic on port 53 to the agent on port 15053 for all servers 467 // in etc/resolv.conf 468 // We avoid redirecting all IP ranges to avoid infinite loops when there are local DNS proxies 469 // such as: app -> istio dns server -> dnsmasq -> upstream 470 // This ensures that we do not get requests from dnsmasq sent back to the agent dns server in a loop. 471 // Note: If a user somehow configured etc/resolv.conf to point to dnsmasq and server X, and dnsmasq also 472 // pointed to server X, this would not work. However, the assumption is that is not a common case. 473 cfg.ruleBuilder.AppendRuleV4(iptableslog.UndefinedCommand, 474 constants.ISTIOOUTPUT, constants.NAT, 475 "-p", constants.TCP, 476 "--dport", "53", 477 "-d", s+"/32", 478 "-j", constants.REDIRECT, 479 "--to-ports", constants.IstioAgentDNSListenerPort) 480 } 481 for _, s := range cfg.cfg.DNSServersV6 { 482 cfg.ruleBuilder.AppendRuleV6(iptableslog.UndefinedCommand, 483 constants.ISTIOOUTPUT, constants.NAT, 484 "-p", constants.TCP, 485 "--dport", "53", 486 "-d", s+"/128", 487 "-j", constants.REDIRECT, 488 "--to-ports", constants.IstioAgentDNSListenerPort) 489 } 490 } 491 } 492 493 // Skip redirection for Envoy-aware applications and 494 // container-to-container traffic both of which explicitly use 495 // localhost. 496 cfg.ruleBuilder.AppendVersionedRule(cfg.cfg.HostIPv4LoopbackCidr, "::1/128", iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 497 "-d", constants.IPVersionSpecific, "-j", constants.RETURN) 498 // Apply outbound IPv4 exclusions. Must be applied before inclusions. 499 for _, cidr := range ipv4RangesExclude.CIDRs { 500 cfg.ruleBuilder.AppendRuleV4(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-d", cidr.String(), "-j", constants.RETURN) 501 } 502 for _, cidr := range ipv6RangesExclude.CIDRs { 503 cfg.ruleBuilder.AppendRuleV6(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-d", cidr.String(), "-j", constants.RETURN) 504 } 505 506 cfg.handleOutboundPortsInclude() 507 508 cfg.handleOutboundIncludeRules(ipv4RangesInclude, cfg.ruleBuilder.AppendRuleV4, cfg.ruleBuilder.InsertRuleV4) 509 cfg.handleOutboundIncludeRules(ipv6RangesInclude, cfg.ruleBuilder.AppendRuleV6, cfg.ruleBuilder.InsertRuleV6) 510 511 if redirectDNS { 512 // Jump from OUTPUT chain to ISTIOOUTPUT chain for all UDP traffic 513 cfg.ruleBuilder.AppendRule(iptableslog.JumpOutbound, constants.OUTPUT, constants.NAT, "-p", constants.UDP, "-j", constants.ISTIOOUTPUT) 514 cfg.ruleBuilder.AppendRule(iptableslog.JumpOutbound, constants.OUTPUT, constants.RAW, "-p", constants.UDP, "-j", constants.ISTIOOUTPUT) 515 516 HandleDNSUDP( 517 AppendOps, cfg.ruleBuilder, cfg.ext, &iptVer, &ipt6Ver, 518 cfg.cfg.ProxyUID, cfg.cfg.ProxyGID, 519 cfg.cfg.DNSServersV4, cfg.cfg.DNSServersV6, cfg.cfg.CaptureAllDNS, 520 ownerGroupsFilter) 521 } 522 523 if cfg.cfg.InboundInterceptionMode == constants.TPROXY { 524 // save packet mark set by envoy.filters.listener.original_src as connection mark 525 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.PREROUTING, constants.MANGLE, 526 "-p", constants.TCP, "-m", "mark", "--mark", cfg.cfg.InboundTProxyMark, "-j", "CONNMARK", "--save-mark") 527 // If the packet is already marked with 1337, then return. This is to prevent mark envoy --> app traffic again. 528 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.OUTPUT, constants.MANGLE, 529 "-p", constants.TCP, "-o", "lo", "-m", "mark", "--mark", cfg.cfg.InboundTProxyMark, "-j", constants.RETURN) 530 for _, uid := range split(cfg.cfg.ProxyUID) { 531 // mark outgoing packets from envoy to workload by pod ip 532 // app call VIP --> envoy outbound -(mark 1338)-> envoy inbound --> app 533 cfg.ruleBuilder.AppendVersionedRule(cfg.cfg.HostIPv4LoopbackCidr, "::1/128", iptableslog.UndefinedCommand, constants.OUTPUT, constants.MANGLE, 534 "!", "-d", constants.IPVersionSpecific, "-p", constants.TCP, "-o", "lo", 535 "-m", "owner", "--uid-owner", uid, "-j", constants.MARK, "--set-mark", outboundMark) 536 } 537 for _, gid := range split(cfg.cfg.ProxyGID) { 538 // mark outgoing packets from envoy to workload by pod ip 539 // app call VIP --> envoy outbound -(mark 1338)-> envoy inbound --> app 540 cfg.ruleBuilder.AppendVersionedRule(cfg.cfg.HostIPv4LoopbackCidr, "::1/128", iptableslog.UndefinedCommand, constants.OUTPUT, constants.MANGLE, 541 "!", "-d", constants.IPVersionSpecific, "-p", constants.TCP, "-o", "lo", 542 "-m", "owner", "--gid-owner", gid, "-j", constants.MARK, "--set-mark", outboundMark) 543 } 544 // mark outgoing packets from workload, match it to policy routing entry setup for TPROXY mode 545 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.OUTPUT, constants.MANGLE, 546 "-p", constants.TCP, "-m", "connmark", "--mark", cfg.cfg.InboundTProxyMark, "-j", "CONNMARK", "--restore-mark") 547 // prevent infinite redirect 548 cfg.ruleBuilder.InsertRule(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.MANGLE, 1, 549 "-p", constants.TCP, "-m", "mark", "--mark", cfg.cfg.InboundTProxyMark, "-j", constants.RETURN) 550 // prevent intercept traffic from envoy/pilot-agent ==> app by 127.0.0.6 --> podip 551 cfg.ruleBuilder.InsertRuleV4(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.MANGLE, 2, 552 "-p", constants.TCP, "-s", "127.0.0.6/32", "-i", "lo", "-j", constants.RETURN) 553 cfg.ruleBuilder.InsertRuleV6(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.MANGLE, 2, 554 "-p", constants.TCP, "-s", "::6/128", "-i", "lo", "-j", constants.RETURN) 555 // prevent intercept traffic from app ==> app by pod ip 556 cfg.ruleBuilder.InsertRule(iptableslog.UndefinedCommand, constants.ISTIOINBOUND, constants.MANGLE, 3, 557 "-p", constants.TCP, "-i", "lo", "-m", "mark", "!", "--mark", outboundMark, "-j", constants.RETURN) 558 } 559 return cfg.executeCommands(&iptVer, &ipt6Ver) 560 } 561 562 type UDPRuleApplier struct { 563 iptables *builder.IptablesRuleBuilder 564 ext dep.Dependencies 565 ops Ops 566 table string 567 chain string 568 iptV *dep.IptablesVersion 569 ipt6V *dep.IptablesVersion 570 } 571 572 func (f UDPRuleApplier) RunV4(args ...string) { 573 switch f.ops { 574 case AppendOps: 575 f.iptables.AppendRuleV4(iptableslog.UndefinedCommand, f.chain, f.table, args...) 576 case DeleteOps: 577 deleteArgs := []string{"-t", f.table, opsToString[f.ops], f.chain} 578 deleteArgs = append(deleteArgs, args...) 579 f.ext.RunQuietlyAndIgnore(constants.IPTables, f.iptV, nil, deleteArgs...) 580 } 581 } 582 583 func (f UDPRuleApplier) RunV6(args ...string) { 584 switch f.ops { 585 case AppendOps: 586 f.iptables.AppendRuleV6(iptableslog.UndefinedCommand, f.chain, f.table, args...) 587 case DeleteOps: 588 deleteArgs := []string{"-t", f.table, opsToString[f.ops], f.chain} 589 deleteArgs = append(deleteArgs, args...) 590 f.ext.RunQuietlyAndIgnore(constants.IPTables, f.ipt6V, nil, deleteArgs...) 591 } 592 } 593 594 func (f UDPRuleApplier) Run(args ...string) { 595 f.RunV4(args...) 596 f.RunV6(args...) 597 } 598 599 func (f UDPRuleApplier) WithChain(chain string) UDPRuleApplier { 600 f.chain = chain 601 return f 602 } 603 604 func (f UDPRuleApplier) WithTable(table string) UDPRuleApplier { 605 f.table = table 606 return f 607 } 608 609 // HandleDNSUDP is a helper function to tackle with DNS UDP specific operations. 610 // This helps the creation logic of DNS UDP rules in sync with the deletion. 611 func HandleDNSUDP( 612 ops Ops, iptables *builder.IptablesRuleBuilder, ext dep.Dependencies, 613 iptV, ipt6V *dep.IptablesVersion, proxyUID, proxyGID string, dnsServersV4 []string, dnsServersV6 []string, captureAllDNS bool, 614 ownerGroupsFilter config.InterceptFilter, 615 ) { 616 // TODO BML drop "UDPRuleApplier", it is a largely useless type. 617 // we do not need a unique type just to apply UDP iptables rules 618 f := UDPRuleApplier{ 619 iptables: iptables, 620 ext: ext, 621 ops: ops, 622 table: constants.NAT, 623 chain: constants.ISTIOOUTPUT, 624 iptV: iptV, 625 ipt6V: ipt6V, 626 } 627 // Make sure that upstream DNS requests from agent/envoy dont get captured. 628 // TODO: add ip6 as well 629 for _, uid := range split(proxyUID) { 630 f.Run("-p", "udp", "--dport", "53", "-m", "owner", "--uid-owner", uid, "-j", constants.RETURN) 631 } 632 for _, gid := range split(proxyGID) { 633 f.Run("-p", "udp", "--dport", "53", "-m", "owner", "--gid-owner", gid, "-j", constants.RETURN) 634 } 635 636 if ownerGroupsFilter.Except { 637 for _, group := range ownerGroupsFilter.Values { 638 f.Run("-p", "udp", "--dport", "53", "-m", "owner", "--gid-owner", group, "-j", constants.RETURN) 639 } 640 } else { 641 groupIsNoneOf := CombineMatchers(ownerGroupsFilter.Values, func(group string) []string { 642 return []string{"-m", "owner", "!", "--gid-owner", group} 643 }) 644 f.Run(Flatten([]string{"-p", "udp", "--dport", "53"}, groupIsNoneOf, []string{"-j", constants.RETURN})...) 645 } 646 647 if captureAllDNS { 648 // Redirect all UDP dns traffic on port 53 to the agent on port 15053 649 // This will be useful for the CNI case where pod DNS server address cannot be decided. 650 f.Run("-p", "udp", "--dport", "53", "-j", constants.REDIRECT, "--to-port", constants.IstioAgentDNSListenerPort) 651 } else { 652 // redirect all UDP dns traffic on port 53 to the agent on port 15053 for all servers 653 // in etc/resolv.conf 654 // We avoid redirecting all IP ranges to avoid infinite loops when there are local DNS proxies 655 // such as: app -> istio dns server -> dnsmasq -> upstream 656 // This ensures that we do not get requests from dnsmasq sent back to the agent dns server in a loop. 657 // Note: If a user somehow configured etc/resolv.conf to point to dnsmasq and server X, and dnsmasq also 658 // pointed to server X, this would not work. However, the assumption is that is not a common case. 659 for _, s := range dnsServersV4 { 660 f.RunV4("-p", "udp", "--dport", "53", "-d", s+"/32", 661 "-j", constants.REDIRECT, "--to-port", constants.IstioAgentDNSListenerPort) 662 } 663 for _, s := range dnsServersV6 { 664 f.RunV6("-p", "udp", "--dport", "53", "-d", s+"/128", 665 "-j", constants.REDIRECT, "--to-port", constants.IstioAgentDNSListenerPort) 666 } 667 } 668 // Split UDP DNS traffic to separate conntrack zones 669 addConntrackZoneDNSUDP(f.WithTable(constants.RAW), proxyUID, proxyGID, dnsServersV4, dnsServersV6, captureAllDNS) 670 } 671 672 // addConntrackZoneDNSUDP is a helper function to add iptables rules to split DNS traffic 673 // in two separate conntrack zones to avoid issues with UDP conntrack race conditions. 674 // Traffic that goes from istio to DNS servers and vice versa are zone 1 and traffic from 675 // DNS client to istio and vice versa goes to zone 2 676 func addConntrackZoneDNSUDP( 677 f UDPRuleApplier, proxyUID, proxyGID string, dnsServersV4 []string, dnsServersV6 []string, captureAllDNS bool, 678 ) { 679 // TODO: add ip6 as well 680 for _, uid := range split(proxyUID) { 681 // Packets with dst port 53 from istio to zone 1. These are Istio calls to upstream resolvers 682 f.Run("-p", "udp", "--dport", "53", "-m", "owner", "--uid-owner", uid, "-j", constants.CT, "--zone", "1") 683 // Packets with src port 15053 from istio to zone 2. These are Istio response packets to application clients 684 f.Run("-p", "udp", "--sport", "15053", "-m", "owner", "--uid-owner", uid, "-j", constants.CT, "--zone", "2") 685 } 686 for _, gid := range split(proxyGID) { 687 // Packets with dst port 53 from istio to zone 1. These are Istio calls to upstream resolvers 688 f.Run("-p", "udp", "--dport", "53", "-m", "owner", "--gid-owner", gid, "-j", constants.CT, "--zone", "1") 689 // Packets with src port 15053 from istio to zone 2. These are Istio response packets to application clients 690 f.Run("-p", "udp", "--sport", "15053", "-m", "owner", "--gid-owner", gid, "-j", constants.CT, "--zone", "2") 691 692 } 693 694 if captureAllDNS { 695 // Not specifying destination address is useful for the CNI case where pod DNS server address cannot be decided. 696 697 // Mark all UDP dns traffic with dst port 53 as zone 2. These are application client packets towards DNS resolvers. 698 f.Run("-p", "udp", "--dport", "53", 699 "-j", constants.CT, "--zone", "2") 700 // Mark all UDP dns traffic with src port 53 as zone 1. These are response packets from the DNS resolvers. 701 f.WithChain(constants.PREROUTING).Run("-p", "udp", "--sport", "53", 702 "-j", constants.CT, "--zone", "1") 703 } else { 704 // Go through all DNS servers in etc/resolv.conf and mark the packets based on these destination addresses. 705 for _, s := range dnsServersV4 { 706 // Mark all UDP dns traffic with dst port 53 as zone 2. These are application client packets towards DNS resolvers. 707 f.RunV4("-p", "udp", "--dport", "53", "-d", s+"/32", 708 "-j", constants.CT, "--zone", "2") 709 // Mark all UDP dns traffic with src port 53 as zone 1. These are response packets from the DNS resolvers. 710 f.WithChain(constants.PREROUTING).RunV4("-p", "udp", "--sport", "53", "-s", s+"/32", 711 "-j", constants.CT, "--zone", "1") 712 } 713 for _, s := range dnsServersV6 { 714 // Mark all UDP dns traffic with dst port 53 as zone 2. These are application client packets towards DNS resolvers. 715 f.RunV6("-p", "udp", "--dport", "53", "-d", s+"/128", 716 "-j", constants.CT, "--zone", "2") 717 // Mark all UDP dns traffic with src port 53 as zone 1. These are response packets from the DNS resolvers. 718 f.WithChain(constants.PREROUTING).RunV6("-p", "udp", "--sport", "53", "-s", s+"/128", 719 "-j", constants.CT, "--zone", "1") 720 } 721 } 722 } 723 724 func (cfg *IptablesConfigurator) handleOutboundPortsInclude() { 725 if cfg.cfg.OutboundPortsInclude != "" { 726 for _, port := range split(cfg.cfg.OutboundPortsInclude) { 727 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, 728 constants.ISTIOOUTPUT, constants.NAT, "-p", constants.TCP, "--dport", port, "-j", constants.ISTIOREDIRECT) 729 } 730 } 731 } 732 733 func (cfg *IptablesConfigurator) handleCaptureByOwnerGroup(filter config.InterceptFilter) { 734 if filter.Except { 735 for _, group := range filter.Values { 736 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 737 "-m", "owner", "--gid-owner", group, "-j", constants.RETURN) 738 } 739 } else { 740 groupIsNoneOf := CombineMatchers(filter.Values, func(group string) []string { 741 return []string{"-m", "owner", "!", "--gid-owner", group} 742 }) 743 cfg.ruleBuilder.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, 744 append(groupIsNoneOf, "-j", constants.RETURN)...) 745 } 746 } 747 748 func (cfg *IptablesConfigurator) executeIptablesCommands(iptVer *dep.IptablesVersion, commands [][]string) error { 749 for _, cmd := range commands { 750 if err := cfg.ext.Run(constants.IPTables, iptVer, nil, cmd...); err != nil { 751 return err 752 } 753 } 754 return nil 755 } 756 757 func (cfg *IptablesConfigurator) executeIptablesRestoreCommand(iptVer *dep.IptablesVersion, isIpv4 bool) error { 758 var data string 759 if isIpv4 { 760 data = cfg.ruleBuilder.BuildV4Restore() 761 } else { 762 data = cfg.ruleBuilder.BuildV6Restore() 763 } 764 765 log.Infof("Running iptables restore with: %s and the following input:\n%v", iptVer.CmdToString(constants.IPTablesRestore), strings.TrimSpace(data)) 766 // --noflush to prevent flushing/deleting previous contents from table 767 return cfg.ext.Run(constants.IPTablesRestore, iptVer, strings.NewReader(data), "--noflush") 768 } 769 770 func (cfg *IptablesConfigurator) executeCommands(iptVer, ipt6Ver *dep.IptablesVersion) error { 771 if cfg.cfg.RestoreFormat { 772 // Execute iptables-restore 773 if err := cfg.executeIptablesRestoreCommand(iptVer, true); err != nil { 774 return err 775 } 776 // Execute ip6tables-restore 777 if err := cfg.executeIptablesRestoreCommand(ipt6Ver, false); err != nil { 778 return err 779 } 780 } else { 781 // Execute iptables commands 782 if err := cfg.executeIptablesCommands(iptVer, cfg.ruleBuilder.BuildV4()); err != nil { 783 return err 784 } 785 // Execute ip6tables commands 786 if err := cfg.executeIptablesCommands(ipt6Ver, cfg.ruleBuilder.BuildV6()); err != nil { 787 return err 788 } 789 } 790 return nil 791 }