Skip to content

Commit 53b2393

Browse files
authored
Merge pull request #45 from aramase/ipv6
Support for IPv6
2 parents e3958d2 + 6a890d4 commit 53b2393

File tree

4 files changed

+234
-27
lines changed

4 files changed

+234
-27
lines changed

Diff for: README.md

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ By default, the agent is configured to reload its configuration from the `/etc/c
2828
The agent configuration file should be written in yaml or json syntax, and may contain three optional keys:
2929
- `nonMasqueradeCIDRs []string`: A list strings in CIDR notation that specify the non-masquerade ranges.
3030
- `masqLinkLocal bool`: Whether to masquerade traffic to `169.254.0.0/16`. False by default.
31+
- `masqLinkLocalIPv6 bool`: Whether to masquerade traffic to `fe80::/10`. False by default.
3132
- `resyncInterval string`: The interval at which the agent attempts to reload config from disk. The syntax is any format accepted by Go's [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) function.
3233

3334
The agent will look for a config file in its container at `/etc/config/ip-masq-agent`. This file can be provided via a `ConfigMap`, plumbed into the container via a `ConfigMapVolumeSource`. As a result, the agent can be reconfigured in a live cluster by creating or editing this `ConfigMap`.
@@ -50,6 +51,8 @@ The agent accepts two flags, which may be specified in the yaml file.
5051
`nomasq-all-reserved-ranges`
5152
: Whether or not to masquerade all RFC reserved ranges when the configmap is empty. The default is `false`. When `false`, the agent will masquerade to every destination except the ranges reserved by RFC 1918 (namely `10.0.0.0/8`, `172.16.0.0/12`, and `192.168.0.0/16`). When `true`, the agent will masquerade to every destination that is not marked reserved by an RFC. The full list of ranges is (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, `192.0.0.0/24`, `192.0.2.0/24`, `192.88.99.0/24`, `198.18.0.0/15`, `198.51.100.0/24`, `203.0.113.0/24`, and `240.0.0.0/4`). Note however, that this list of ranges is overridden by specifying the nonMasqueradeCIDRs key in the agent configmap.
5253

54+
`enable-ipv6`
55+
: Whether to configurate ip6tables rules. By default `enable-ipv6` is false.
5356

5457
## Rationale
5558
(from the [incubator proposal](https://gist.github.com/mtaufen/253309166e7d5aa9e9b560600a438447))

Diff for: agent-config/config

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ nonMasqueradeCIDRs:
33
- 172.16.0.0/12
44
- 192.168.0.0/16
55
masqLinkLocal: false
6+
masqLinkLocalIPv6: false
67
resyncInterval: 60s

Diff for: cmd/ip-masq-agent/ip-masq-agent.go

+102-13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import (
3939

4040
const (
4141
linkLocalCIDR = "169.254.0.0/16"
42+
// RFC 4291
43+
linkLocalCIDRIPv6 = "fe80::/10"
4244
// path to a yaml or json file
4345
configPath = "/etc/config/ip-masq-agent"
4446
)
@@ -48,18 +50,21 @@ var (
4850
masqChain utiliptables.Chain
4951
masqChainFlag = flag.String("masq-chain", "IP-MASQ-AGENT", `Name of nat chain for iptables masquerade rules.`)
5052
noMasqueradeAllReservedRangesFlag = flag.Bool("nomasq-all-reserved-ranges", false, "Whether to disable masquerade for all IPv4 ranges reserved by RFCs.")
53+
enableIPv6 = flag.Bool("enable-ipv6", false, "Whether to enable IPv6.")
5154
)
5255

53-
// config object
56+
// MasqConfig object
5457
type MasqConfig struct {
5558
NonMasqueradeCIDRs []string `json:"nonMasqueradeCIDRs"`
5659
MasqLinkLocal bool `json:"masqLinkLocal"`
60+
MasqLinkLocalIPv6 bool `json:"masqLinkLocalIPv6"`
5761
ResyncInterval Duration `json:"resyncInterval"`
5862
}
5963

60-
// Go's JSON unmarshaler can't handle time.ParseDuration syntax when unmarshaling into time.Duration, so we do it here
64+
// Duration - Go's JSON unmarshaler can't handle time.ParseDuration syntax when unmarshaling into time.Duration, so we do it here
6165
type Duration time.Duration
6266

67+
// UnmarshalJSON ...
6368
func (d *Duration) UnmarshalJSON(json []byte) error {
6469
if json[0] == '"' {
6570
s := string(json[1 : len(json)-1])
@@ -74,7 +79,7 @@ func (d *Duration) UnmarshalJSON(json []byte) error {
7479
return fmt.Errorf("expected string value for unmarshal to field of type Duration, got %q", s)
7580
}
7681

77-
// returns a MasqConfig with default values
82+
// NewMasqConfig returns a MasqConfig with default values
7883
func NewMasqConfig(masqAllReservedRanges bool) *MasqConfig {
7984
// RFC 1918 defines the private ip address space as 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
8085
nonMasq := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
@@ -94,25 +99,30 @@ func NewMasqConfig(masqAllReservedRanges bool) *MasqConfig {
9499
return &MasqConfig{
95100
NonMasqueradeCIDRs: nonMasq,
96101
MasqLinkLocal: false,
102+
MasqLinkLocalIPv6: false,
97103
ResyncInterval: Duration(60 * time.Second),
98104
}
99105
}
100106

101-
// daemon object
107+
// MasqDaemon object
102108
type MasqDaemon struct {
103-
config *MasqConfig
104-
iptables utiliptables.Interface
109+
config *MasqConfig
110+
iptables utiliptables.Interface
111+
ip6tables utiliptables.Interface
105112
}
106113

107-
// returns a MasqDaemon with default values, including an initialized utiliptables.Interface
114+
// NewMasqDaemon returns a MasqDaemon with default values, including an initialized utiliptables.Interface
108115
func NewMasqDaemon(c *MasqConfig) *MasqDaemon {
109116
execer := utilexec.New()
110117
dbus := utildbus.New()
111-
protocol := utiliptables.ProtocolIpv4
112-
iptables := utiliptables.New(execer, dbus, protocol)
118+
protocolv4 := utiliptables.ProtocolIpv4
119+
protocolv6 := utiliptables.ProtocolIpv6
120+
iptables := utiliptables.New(execer, dbus, protocolv4)
121+
ip6tables := utiliptables.New(execer, dbus, protocolv6)
113122
return &MasqDaemon{
114-
config: c,
115-
iptables: iptables,
123+
config: c,
124+
iptables: iptables,
125+
ip6tables: ip6tables,
116126
}
117127
}
118128

@@ -131,6 +141,7 @@ func main() {
131141
m.Run()
132142
}
133143

144+
// Run ...
134145
func (m *MasqDaemon) Run() {
135146
// Periodically resync to reconfigure or heal from any rule decay
136147
for {
@@ -146,6 +157,11 @@ func (m *MasqDaemon) Run() {
146157
glog.Errorf("error syncing masquerade rules: %v", err)
147158
return
148159
}
160+
// resync ipv6 rules
161+
if err := m.syncMasqRulesIPv6(); err != nil {
162+
glog.Errorf("error syncing masquerade rules for ipv6: %v", err)
163+
return
164+
}
149165
}()
150166
}
151167
}
@@ -174,6 +190,7 @@ func (m *MasqDaemon) syncConfig(fs fakefs.FileSystem) error {
174190
// file does not exist, use defaults
175191
m.config.NonMasqueradeCIDRs = c.NonMasqueradeCIDRs
176192
m.config.MasqLinkLocal = c.MasqLinkLocal
193+
m.config.MasqLinkLocalIPv6 = c.MasqLinkLocalIPv6
177194
m.config.ResyncInterval = c.ResyncInterval
178195
glog.V(2).Infof("no config file found at %q, using default values", configPath)
179196
return nil
@@ -210,13 +227,17 @@ func (c *MasqConfig) validate() error {
210227
// limit to 64 CIDRs (excluding link-local) to protect against really bad mistakes
211228
n := len(c.NonMasqueradeCIDRs)
212229
if n > 64 {
213-
return fmt.Errorf("The daemon can only accept up to 64 CIDRs (excluding link-local), but got %d CIDRs (excluding link local).", n)
230+
return fmt.Errorf("the daemon can only accept up to 64 CIDRs (excluding link-local), but got %d CIDRs (excluding link local)", n)
214231
}
215232
// check CIDRs are valid
216233
for _, cidr := range c.NonMasqueradeCIDRs {
217234
if err := validateCIDR(cidr); err != nil {
218235
return err
219236
}
237+
// can't configure ipv6 cidr if ipv6 is not enabled
238+
if !*enableIPv6 && isIPv6CIDR(cidr) {
239+
return fmt.Errorf("ipv6 is not enabled, but ipv6 cidr %s provided. Enable ipv6 using --enable-ipv6 agent flag", cidr)
240+
}
220241
}
221242
return nil
222243
}
@@ -258,19 +279,63 @@ func (m *MasqDaemon) syncMasqRules() error {
258279

259280
// non-masquerade for user-provided CIDRs
260281
for _, cidr := range m.config.NonMasqueradeCIDRs {
261-
writeNonMasqRule(lines, cidr)
282+
if !isIPv6CIDR(cidr) {
283+
writeNonMasqRule(lines, cidr)
284+
}
262285
}
263286

264287
// masquerade all other traffic that is not bound for a --dst-type LOCAL destination
265288
writeMasqRule(lines)
266289

267290
writeLine(lines, "COMMIT")
291+
268292
if err := m.iptables.RestoreAll(lines.Bytes(), utiliptables.NoFlushTables, utiliptables.NoRestoreCounters); err != nil {
269293
return err
270294
}
271295
return nil
272296
}
273297

298+
func (m *MasqDaemon) syncMasqRulesIPv6() error {
299+
isIPv6Enabled := *enableIPv6
300+
301+
if isIPv6Enabled {
302+
// make sure our custom chain for ipv6 non-masquerade exists
303+
_, err := m.ip6tables.EnsureChain(utiliptables.TableNAT, masqChain)
304+
if err != nil {
305+
return err
306+
}
307+
// ensure that any non-local in POSTROUTING jumps to masqChain
308+
if err := m.ensurePostroutingJumpIPv6(); err != nil {
309+
return err
310+
}
311+
// build up lines to pass to ip6tables-restore
312+
lines6 := bytes.NewBuffer(nil)
313+
writeLine(lines6, "*nat")
314+
writeLine(lines6, utiliptables.MakeChainLine(masqChain)) // effectively flushes masqChain atomically with rule restore
315+
316+
// link-local IPv6 CIDR is non-masquerade by default
317+
if !m.config.MasqLinkLocalIPv6 {
318+
writeNonMasqRule(lines6, linkLocalCIDRIPv6)
319+
}
320+
321+
for _, cidr := range m.config.NonMasqueradeCIDRs {
322+
if isIPv6CIDR(cidr) {
323+
writeNonMasqRule(lines6, cidr)
324+
}
325+
}
326+
327+
// masquerade all other traffic that is not bound for a --dst-type LOCAL destination
328+
writeMasqRule(lines6)
329+
330+
writeLine(lines6, "COMMIT")
331+
332+
if err := m.ip6tables.RestoreAll(lines6.Bytes(), utiliptables.NoFlushTables, utiliptables.NoRestoreCounters); err != nil {
333+
return err
334+
}
335+
}
336+
return nil
337+
}
338+
274339
// NOTE(mtaufen): iptables requires names to be <= 28 characters, and somehow prepending "-m comment --comment " to this string makes it think this condition is violated
275340
// Feel free to dig around in iptables and see if you can figure out exactly why; I haven't had time to fully trace how it parses and handle subcommands.
276341
// If you want to investigate, get the source via `git clone git://git.netfilter.org/iptables.git`, `git checkout v1.4.21` (the version I've seen this issue on,
@@ -288,6 +353,15 @@ func (m *MasqDaemon) ensurePostroutingJump() error {
288353
return nil
289354
}
290355

356+
func (m *MasqDaemon) ensurePostroutingJumpIPv6() error {
357+
if _, err := m.ip6tables.EnsureRule(utiliptables.Append, utiliptables.TableNAT, utiliptables.ChainPostrouting,
358+
"-m", "comment", "--comment", postroutingJumpComment(),
359+
"-m", "addrtype", "!", "--dst-type", "LOCAL", "-j", string(masqChain)); err != nil {
360+
return fmt.Errorf("failed to ensure that %s chain %s jumps to MASQUERADE: %v for ipv6", utiliptables.TableNAT, masqChain, err)
361+
}
362+
return nil
363+
}
364+
291365
const nonMasqRuleComment = `-m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE"`
292366

293367
func writeNonMasqRule(lines *bytes.Buffer, cidr string) {
@@ -311,3 +385,18 @@ func writeRule(lines *bytes.Buffer, position utiliptables.RulePosition, chain ut
311385
func writeLine(lines *bytes.Buffer, words ...string) {
312386
lines.WriteString(strings.Join(words, " ") + "\n")
313387
}
388+
389+
// isIPv6CIDR checks if the provided cidr block belongs to ipv6 family.
390+
// If cidr belongs to ipv6 family, return true else it returns false
391+
// which means the cidr belongs to ipv4 family
392+
func isIPv6CIDR(cidr string) bool {
393+
ip, _, _ := net.ParseCIDR(cidr)
394+
return isIPv6(ip.String())
395+
}
396+
397+
// isIPv6 checks if the provided ip belongs to ipv6 family.
398+
// If ip belongs to ipv6 family, return true else it returns false
399+
// which means the ip belongs to ipv4 family
400+
func isIPv6(ip string) bool {
401+
return net.ParseIP(ip).To4() == nil
402+
}

0 commit comments

Comments
 (0)