Skip to content

link: add support of ipip interface#33

Merged
cathay4t merged 2 commits into
rust-netlink:mainfrom
cathay4t:main
Jun 10, 2026
Merged

link: add support of ipip interface#33
cathay4t merged 2 commits into
rust-netlink:mainfrom
cathay4t:main

Conversation

@cathay4t

Copy link
Copy Markdown
Member

Add integration tests covering all ipip tunnel options.

cathay4t added 2 commits June 10, 2026 06:38
Signed-off-by: Gris Ge <cnfourt@gmail.com>
Add integration tests covering all ipip tunnel options.

Signed-off-by: Gris Ge <cnfourt@gmail.com>
@cathay4t
cathay4t enabled auto-merge (rebase) June 10, 2026 00:37
@cathay4t
cathay4t merged commit d716f1e into rust-netlink:main Jun 10, 2026
3 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for IP-in-IP (IpIp) tunnel interfaces, including configuration parsing, formatting, and unit tests. The review identified a critical compilation error in show.rs due to a temporary value lifetime issue, which can be resolved using std::borrow::Cow. Additionally, there is a correctness bug in iptun.rs where encapsulation flags overwrite each other instead of accumulating, and a behavior mismatch where the path MTU discovery fallback should default to enabled to match iproute2. A minor typo was also noted in AGENTS.md.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/ip/link/show.rs
Comment on lines 88 to 98
let display_name = if let Some(link_name) = &self.link {
link_name
} else if let Some(link_index) = self.link_index {
&format!("if{link_index}")
if link_index == 0 {
"NONE"
} else {
&format!("if{link_index}")
}
} else {
"NONE"
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This nested if expression inside the else if block will cause a compilation error: temporary value dropped while still in use.

In Rust, temporary lifetime extension only applies to the immediate initializing expression of a let binding. Because the format! macro is now nested inside an inner if/else block, the temporary String is dropped at the end of the else block, leaving display_name as a dangling reference.

To fix this, we can use std::borrow::Cow<'_: str> to represent either a borrowed or owned string, which avoids unnecessary allocations while resolving the lifetime issue.

Suggested change
let display_name = if let Some(link_name) = &self.link {
link_name
} else if let Some(link_index) = self.link_index {
&format!("if{link_index}")
if link_index == 0 {
"NONE"
} else {
&format!("if{link_index}")
}
} else {
"NONE"
};
let display_name = if let Some(link_name) = &self.link {
std::borrow::Cow::Borrowed(link_name.as_str())
} else if let Some(link_index) = self.link_index {
if link_index == 0 {
std::borrow::Cow::Borrowed("NONE")
} else {
std::borrow::Cow::Owned(format!("if{link_index}"))
}
} else {
std::borrow::Cow::Borrowed("NONE")
};

Comment on lines +235 to +402
let mut builder = LinkIpIp::new(&self.name);
let mut metadata = false;

let mut iter = self.iface_specific.iter();
while let Some(key) = iter.next() {
let mut next_val = || {
iter.next().ok_or_else(|| {
CliError::from(format!("ipip {key} requires a value"))
})
};
match key.as_str() {
"local" => {
let v = next_val()?;
let addr: Ipv4Addr = parse_ip(v, "local")?;
builder = builder.local(addr);
}
"remote" => {
let v = next_val()?;
let addr: Ipv4Addr = parse_ip(v, "remote")?;
builder = builder.remote(addr);
}
"dev" => {
let v = next_val()?;
let ifindex = self.get_ifindex_by_name(handle, v).await?;
builder = builder.dev(ifindex);
}
"ttl" | "hoplimit" | "hlim" => {
let v = next_val()?;
match v.as_str() {
"inherit" => {
builder = builder.ttl(0);
}
_ => {
let ttl: u8 = v.parse().map_err(|_| {
CliError::from(format!("invalid TTL: {v}"))
})?;
builder = builder.ttl(ttl);
}
}
}
"tos" | "tclass" | "tc" | "dsfield" => {
let v = next_val()?;
match v.as_str() {
"inherit" => {
builder = builder.tos(1);
}
_ => {
let tos: u8 = parse_dsfield(v)?;
builder = builder.tos(tos);
}
}
}
"pmtudisc" => {
builder = builder.pmtudisc(true);
}
"nopmtudisc" => {
builder = builder.pmtudisc(false);
}
"mode" => {
let v = next_val()?;
match v.as_str() {
"ipip" | "ipv4/ipv4" | "ip4ip4" => {
builder = builder.protocol(IpProtocol::Ipip);
}
"mplsip" | "mpls/ipv4" => {
let proto = IpProtocol::from(137u8);
builder = builder.protocol(proto);
}
"any" | "any/ipv4" => {
let proto = IpProtocol::from(0u8);
builder = builder.protocol(proto);
}
_ => {
return Err(CliError::from(format!(
"Cannot guess tunnel mode: {v}"
)));
}
}
}
"external" => {
metadata = true;
}
"noencap" => {
builder = builder.encap_type(TunnelEncapType::None);
}
"encap" => {
let v = next_val()?;
match v.as_str() {
"fou" => {
builder = builder.encap_type(TunnelEncapType::Fou);
}
"gue" => {
builder = builder.encap_type(TunnelEncapType::Gue);
}
"none" => {
builder = builder.encap_type(TunnelEncapType::None);
}
_ => {
return Err(CliError::from(format!(
"Invalid encap type: {v}"
)));
}
}
}
"encap-sport" => {
let v = next_val()?;
if v == "auto" {
builder = builder.encap_sport(0);
} else {
let port = parse_u16(v, "encap-sport")?;
builder = builder.encap_sport(port);
}
}
"encap-dport" => {
let v = next_val()?;
let port = parse_u16(v, "encap-dport")?;
builder = builder.encap_dport(port);
}
"encap-csum" => {
let flags = TunnelEncapFlags::CSum;
builder = builder.encap_flags(flags);
}
"noencap-csum" => {
let flags = TunnelEncapFlags::empty();
builder = builder.encap_flags(flags);
}
"encap-udp6-csum" => {
let flags = TunnelEncapFlags::CSum6;
builder = builder.encap_flags(flags);
}
"noencap-udp6-csum" => {
let flags = TunnelEncapFlags::empty();
builder = builder.encap_flags(flags);
}
"encap-remcsum" => {
let flags = TunnelEncapFlags::RemCSum;
builder = builder.encap_flags(flags);
}
"noencap-remcsum" => {
let flags = TunnelEncapFlags::empty();
builder = builder.encap_flags(flags);
}
"fwmark" => {
let v = next_val()?;
let mark = if let Some(hex) = v.strip_prefix("0x") {
u32::from_str_radix(hex, 16)
} else {
v.parse()
};
let mark = mark.map_err(|_| {
CliError::from(format!("invalid fwmark: {v}"))
})?;
builder = builder.fwmark(mark);
}
_ => {
return Err(CliError::from(format!(
"Unknown ipip argument: {key}"
)));
}
}
}

if metadata {
builder = builder.collect_metadata(true);
}

Ok(builder)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a correctness bug in how encapsulation flags are handled.

Currently, each encapsulation flag option (e.g., encap-csum, encap-udp6-csum, encap-remcsum) completely overwrites the previous flags because builder.encap_flags(flags) replaces the entire flags field rather than combining them. Additionally, the noencap-* options clear all flags by setting them to TunnelEncapFlags::empty(), rather than just clearing the specific flag.

To fix this, we should accumulate the flags in a local encap_flags variable and apply them to the builder after the loop.

        let mut builder = LinkIpIp::new(&self.name);
        let mut metadata = false;
        let mut encap_flags = TunnelEncapFlags::empty();
        let mut has_encap_flags = false;

        let mut iter = self.iface_specific.iter();
        while let Some(key) = iter.next() {
            let mut next_val = || {
                iter.next().ok_or_else(|| {
                    CliError::from(format!("ipip {key} requires a value"))
                })
            };
            match key.as_str() {
                "local" => {
                    let v = next_val()?;
                    let addr: Ipv4Addr = parse_ip(v, "local")?;
                    builder = builder.local(addr);
                }
                "remote" => {
                    let v = next_val()?;
                    let addr: Ipv4Addr = parse_ip(v, "remote")?;
                    builder = builder.remote(addr);
                }
                "dev" => {
                    let v = next_val()?;
                    let ifindex = self.get_ifindex_by_name(handle, v).await?;
                    builder = builder.dev(ifindex);
                }
                "ttl" | "hoplimit" | "hlim" => {
                    let v = next_val()?;
                    match v.as_str() {
                        "inherit" => {
                            builder = builder.ttl(0);
                        }
                        _ => {
                            let ttl: u8 = v.parse().map_err(|_| {
                                CliError::from(format!("invalid TTL: {v}"))
                            })?;
                            builder = builder.ttl(ttl);
                        }
                    }
                }
                "tos" | "tclass" | "tc" | "dsfield" => {
                    let v = next_val()?;
                    match v.as_str() {
                        "inherit" => {
                            builder = builder.tos(1);
                        }
                        _ => {
                            let tos: u8 = parse_dsfield(v)?;
                            builder = builder.tos(tos);
                        }
                    }
                }
                "pmtudisc" => {
                    builder = builder.pmtudisc(true);
                }
                "nopmtudisc" => {
                    builder = builder.pmtudisc(false);
                }
                "mode" => {
                    let v = next_val()?;
                    match v.as_str() {
                        "ipip" | "ipv4/ipv4" | "ip4ip4" => {
                            builder = builder.protocol(IpProtocol::Ipip);
                        }
                        "mplsip" | "mpls/ipv4" => {
                            let proto = IpProtocol::from(137u8);
                            builder = builder.protocol(proto);
                        }
                        "any" | "any/ipv4" => {
                            let proto = IpProtocol::from(0u8);
                            builder = builder.protocol(proto);
                        }
                        _ => {
                            return Err(CliError::from(format!(
                                "Cannot guess tunnel mode: {v}"
                            )));
                        }
                    }
                }
                "external" => {
                    metadata = true;
                }
                "noencap" => {
                    builder = builder.encap_type(TunnelEncapType::None);
                }
                "encap" => {
                    let v = next_val()?;
                    match v.as_str() {
                        "fou" => {
                            builder = builder.encap_type(TunnelEncapType::Fou);
                        }
                        "gue" => { 
                            builder = builder.encap_type(TunnelEncapType::Gue);
                        }
                        "none" => {
                            builder = builder.encap_type(TunnelEncapType::None);
                        }
                        _ => {
                            return Err(CliError::from(format!(
                                "Invalid encap type: {v}"
                            )));
                        }
                    }
                }
                "encap-sport" => {
                    let v = next_val()?;
                    if v == "auto" {
                        builder = builder.encap_sport(0);
                    } else {
                        let port = parse_u16(v, "encap-sport")?;
                        builder = builder.encap_sport(port);
                    }
                }
                "encap-dport" => {
                    let v = next_val()?;
                    let port = parse_u16(v, "encap-dport")?;
                    builder = builder.encap_dport(port);
                }
                "encap-csum" => {
                    encap_flags.insert(TunnelEncapFlags::CSum);
                    has_encap_flags = true;
                }
                "noencap-csum" => {
                    encap_flags.remove(TunnelEncapFlags::CSum);
                    has_encap_flags = true;
                }
                "encap-udp6-csum" => {
                    encap_flags.insert(TunnelEncapFlags::CSum6);
                    has_encap_flags = true;
                }
                "noencap-udp6-csum" => {
                    encap_flags.remove(TunnelEncapFlags::CSum6);
                    has_encap_flags = true;
                }
                "encap-remcsum" => {
                    encap_flags.insert(TunnelEncapFlags::RemCSum);
                    has_encap_flags = true;
                }
                "noencap-remcsum" => {
                    encap_flags.remove(TunnelEncapFlags::RemCSum);
                    has_encap_flags = true;
                }
                "fwmark" => {
                    let v = next_val()?;
                    let mark = if let Some(hex) = v.strip_prefix("0x") {
                        u32::from_str_radix(hex, 16)
                    } else {
                        v.parse()
                    };
                    let mark = mark.map_err(|_| {
                        CliError::from(format!("invalid fwmark: {v}"))
                    })?;
                    builder = builder.fwmark(mark);
                }
                _ => {
                    return Err(CliError::from(format!(
                        "Unknown ipip argument: {key}"
                    )));
                }
            }
        }

        if has_encap_flags {
            builder = builder.encap_flags(encap_flags);
        }

        if metadata {
            builder = builder.collect_metadata(true);
        }

        Ok(builder)

Comment on lines +180 to +188
if let Some(pmtudisc) = self.pmtudisc {
if pmtudisc {
emit!("pmtudisc");
} else {
emit!("nopmtudisc");
}
} else {
emit!("nopmtudisc");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In iproute2, if the IFLA_IPTUN_PMTUDISC attribute is missing, it defaults to enabling path MTU discovery (pmtudisc).

Currently, the fallback defaults to nopmtudisc when self.pmtudisc is None. We should change this to pmtudisc to match the behavior of iproute2.

Suggested change
if let Some(pmtudisc) = self.pmtudisc {
if pmtudisc {
emit!("pmtudisc");
} else {
emit!("nopmtudisc");
}
} else {
emit!("nopmtudisc");
}
if let Some(pmtudisc) = self.pmtudisc {
if pmtudisc {
emit!("pmtudisc");
} else {
emit!("nopmtudisc");
}
} else {
emit!("pmtudisc");
}

Comment thread AGENTS.md

* Netlink attribute parsing and emitting code should be done by other
rust-netlink crates which are also locally maintained, change code in
local folder and use local folder temporally before upstream merge.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Typo: "temporally" should be "temporarily".

Suggested change
local folder and use local folder temporally before upstream merge.
local folder and use local folder temporarily before upstream merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant