github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/firewall/outgoing_firewall_iptables.go (about) 1 /* 2 * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package firewall 19 20 import ( 21 "net/url" 22 "strings" 23 "sync" 24 25 "github.com/mysteriumnetwork/node/firewall/iptables" 26 "github.com/rs/zerolog/log" 27 ) 28 29 const killswitchChain = "MYST_CONSUMER_KILL_SWITCH" 30 31 type refCount struct { 32 count int 33 f func() 34 } 35 36 type outgoingFirewallIptables struct { 37 lock sync.Mutex 38 trafficLockScope Scope 39 referenceTracker map[string]refCount 40 } 41 42 // Setup tries to setup all changes made by setup and leave system in the state before setup. 43 func (obi *outgoingFirewallIptables) Setup() error { 44 if err := obi.checkIptablesVersion(); err != nil { 45 return err 46 } 47 if err := obi.cleanupStaleRules(); err != nil { 48 return err 49 } 50 return obi.setupKillSwitchChain() 51 } 52 53 // Teardown tries to cleanup all changes made by setup and leave system in the state before setup. 54 func (obi *outgoingFirewallIptables) Teardown() { 55 if err := obi.cleanupStaleRules(); err != nil { 56 log.Warn().Err(err).Msg("Error cleaning up iptables rules, you might want to do it yourself") 57 } 58 } 59 60 // BlockOutgoingTraffic effectively disallows any outgoing traffic from consumer node with specified scope. 61 func (obi *outgoingFirewallIptables) BlockOutgoingTraffic(scope Scope, outboundIP string) (OutgoingRuleRemove, error) { 62 if obi.trafficLockScope == Global { 63 // nothing can override global lock 64 return func() {}, nil 65 } 66 obi.trafficLockScope = scope 67 return obi.trackingReferenceCall("block-traffic", func() (OutgoingRuleRemove, error) { 68 // Take custom chain into effect for packets in OUTPUT 69 return iptables.AddRuleWithRemoval( 70 iptables.AppendTo("OUTPUT").RuleSpec("-s", outboundIP, "-j", killswitchChain), 71 ) 72 }) 73 } 74 75 // AllowIPAccess adds exception to blocked traffic for specified URL (host part is usually taken). 76 func (obi *outgoingFirewallIptables) AllowIPAccess(ip string) (OutgoingRuleRemove, error) { 77 return obi.trackingReferenceCall("allow:"+ip, func() (rule OutgoingRuleRemove, e error) { 78 return iptables.AddRuleWithRemoval( 79 iptables.InsertAt(killswitchChain, 1).RuleSpec("-d", ip, "-j", "ACCEPT"), 80 ) 81 }) 82 } 83 84 // AllowURLAccess adds URL based exception. 85 func (obi *outgoingFirewallIptables) AllowURLAccess(rawURLs ...string) (OutgoingRuleRemove, error) { 86 var ruleRemovers []func() 87 removeAll := func() { 88 for _, ruleRemover := range ruleRemovers { 89 ruleRemover() 90 } 91 } 92 for _, rawURL := range rawURLs { 93 parsed, err := url.Parse(rawURL) 94 if err != nil { 95 removeAll() 96 return nil, err 97 } 98 99 remover, err := obi.AllowIPAccess(parsed.Hostname()) 100 if err != nil { 101 removeAll() 102 return nil, err 103 } 104 ruleRemovers = append(ruleRemovers, remover) 105 } 106 return removeAll, nil 107 } 108 109 func (obi *outgoingFirewallIptables) checkIptablesVersion() error { 110 output, err := iptables.Exec("--version") 111 if err != nil { 112 return err 113 } 114 for _, line := range output { 115 log.Info().Msg("[version check] " + line) 116 } 117 return nil 118 } 119 120 func (obi *outgoingFirewallIptables) setupKillSwitchChain() error { 121 // Add chain 122 if _, err := iptables.Exec("-N", killswitchChain); err != nil { 123 return err 124 } 125 // Append rule - by default all packets going to kill switch chain are rejected 126 if _, err := iptables.Exec("-A", killswitchChain, "-m", "conntrack", "--ctstate", "NEW", "-j", "REJECT"); err != nil { 127 return err 128 } 129 130 // Insert rule - TODO for now always allow outgoing DNS traffic, BUT it should be exposed as separate firewall call 131 if _, err := iptables.Exec("-I", killswitchChain, "1", "-p", "udp", "--dport", "53", "-j", "ACCEPT"); err != nil { 132 return err 133 } 134 // Insert rule - TCP DNS is not so popular - but for the sake of humanity, lets allow it too 135 if _, err := iptables.Exec("-I", killswitchChain, "1", "-p", "tcp", "--dport", "53", "-j", "ACCEPT"); err != nil { 136 return err 137 } 138 139 return nil 140 } 141 142 func (obi *outgoingFirewallIptables) cleanupStaleRules() error { 143 // List rules 144 rules, err := iptables.Exec("-S", "OUTPUT") 145 if err != nil { 146 return err 147 } 148 for _, rule := range rules { 149 // detect if any references exist in OUTPUT chain like -j MYST_CONSUMER_KILL_SWITCH 150 if strings.HasSuffix(rule, killswitchChain) { 151 deleteRule := strings.Replace(rule, "-A", "-D", 1) 152 deleteRuleArgs := strings.Split(deleteRule, " ") 153 if _, err := iptables.Exec(deleteRuleArgs...); err != nil { 154 return err 155 } 156 } 157 } 158 159 // List chain rules 160 if _, err := iptables.Exec("-L", killswitchChain); err != nil { 161 // error means no such chain - log error just in case and bail out 162 log.Info().Err(err).Msg("[setup] Got error while listing kill switch chain rules. Probably nothing to worry about") 163 return nil 164 } 165 166 // Remove chain rules 167 if _, err := iptables.Exec("-F", killswitchChain); err != nil { 168 return err 169 } 170 171 // Remove chain 172 _, err = iptables.Exec("-X", killswitchChain) 173 return err 174 } 175 176 func (obi *outgoingFirewallIptables) trackingReferenceCall(ref string, actualCall func() (OutgoingRuleRemove, error)) (OutgoingRuleRemove, error) { 177 obi.lock.Lock() 178 defer obi.lock.Unlock() 179 180 refCount := obi.referenceTracker[ref] 181 if refCount.count == 0 { 182 removeRule, err := actualCall() 183 if err != nil { 184 return nil, err 185 } 186 refCount.f = removeRule 187 188 refCount.count++ 189 obi.referenceTracker[ref] = refCount 190 } 191 192 return obi.decreaseRefCall(ref), nil 193 } 194 195 func (obi *outgoingFirewallIptables) decreaseRefCall(ref string) OutgoingRuleRemove { 196 return func() { 197 obi.lock.Lock() 198 defer obi.lock.Unlock() 199 200 refCount := obi.referenceTracker[ref] 201 if refCount.count == 1 { 202 refCount.f() 203 204 refCount.count-- 205 obi.referenceTracker[ref] = refCount 206 } 207 } 208 } 209 210 var _ OutgoingTrafficFirewall = &outgoingFirewallIptables{}