]> git.proxmox.com Git - proxmox-backup.git/commitdiff
src/config/network.rs: read/write /etc/network/interfaces
authorDietmar Maurer <dietmar@proxmox.com>
Mon, 20 Apr 2020 12:15:57 +0000 (14:15 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Mon, 20 Apr 2020 12:15:57 +0000 (14:15 +0200)
Start implementing a recursive descent parser.

src/config.rs
src/config/network.rs [new file with mode: 0644]
src/config/network/helper.rs [new file with mode: 0644]
src/config/network/lexer.rs [new file with mode: 0644]
src/config/network/parser.rs [new file with mode: 0644]

index 48d1e8e9ffdd6f3cc21acfd506069dbeca34662d..a9dc1afd8fd7ae66e6a508f9a7fabca5c8eeb019 100644 (file)
@@ -20,6 +20,7 @@ pub mod remote;
 pub mod user;
 pub mod acl;
 pub mod cached_user_info;
+pub mod network;
 
 /// Check configuration directory permissions
 ///
diff --git a/src/config/network.rs b/src/config/network.rs
new file mode 100644 (file)
index 0000000..3751277
--- /dev/null
@@ -0,0 +1,289 @@
+use std::io::{Write};
+use std::collections::{HashSet, HashMap};
+
+use anyhow::{Error, bail};
+
+mod helper;
+//pub use helper::*;
+
+mod lexer;
+pub use lexer::*;
+
+mod parser;
+pub use parser::*;
+
+#[derive(Debug, PartialEq)]
+pub enum AddressFamily {
+    Inet4and6, // both v4 and v6
+    Inet, // v4
+    Inet6, // v6
+}
+
+#[derive(Debug)]
+pub struct Interface {
+    pub name: String,
+    pub address_family: AddressFamily,
+    pub method_v4: Option<Token>,
+    pub method_v6: Option<Token>,
+    pub address_v4: Option<String>,
+    pub gateway_v4: Option<String>,
+    pub netmask_v4: Option<u8>,
+    pub address_v6: Option<String>,
+    pub gateway_v6: Option<String>,
+    pub netmask_v6: Option<u8>,
+    pub options_v4: Vec<String>,
+    pub options_v6: Vec<String>,
+}
+
+impl Interface {
+
+    pub fn new(
+        name: String,
+        address_family: AddressFamily,
+        config_method: Option<Token>,
+    ) -> Self {
+        let config_method_v4 = match address_family {
+            AddressFamily::Inet | AddressFamily::Inet4and6 => Some(config_method.unwrap_or(Token::Static)),
+            _ => None,
+        };
+        let config_method_v6 = match address_family {
+            AddressFamily::Inet6 | AddressFamily::Inet4and6 => Some(config_method.unwrap_or(Token::Static)),
+            _ => None,
+        };
+
+        Self {
+            name,
+            address_family,
+            method_v4: config_method_v4,
+            method_v6: config_method_v6,
+            address_v4: None,
+            gateway_v4: None,
+            netmask_v4: None,
+            address_v6: None,
+            gateway_v6: None,
+            netmask_v6: None,
+            options_v4: Vec::new(),
+            options_v6: Vec::new(),
+        }
+    }
+
+    fn set_address_v4(&mut self, address: String) -> Result<(), Error> {
+        if self.address_v4.is_none() {
+            self.address_v4 = Some(address);
+        } else {
+            bail!("duplicate IPv4 address.");
+        }
+        Ok(())
+    }
+
+    fn set_gateway_v4(&mut self, gateway: String) -> Result<(), Error> {
+        if self.gateway_v4.is_none() {
+            self.gateway_v4 = Some(gateway);
+        } else {
+            bail!("duplicate IPv4 gateway.");
+        }
+        Ok(())
+    }
+
+    fn set_netmask_v4(&mut self, mask: u8) -> Result<(), Error> {
+        if self.netmask_v4.is_none() {
+            if mask > 0 && mask <= 32 {
+                self.netmask_v4 = Some(mask);
+            } else {
+                bail!("invalid ipv4 netmaks '{}'", mask);
+            }
+        } else {
+            bail!("duplicate IPv4 netmask.");
+        }
+        Ok(())
+    }
+
+    fn set_address_v6(&mut self, address: String) -> Result<(), Error> {
+        if self.address_v6.is_none() {
+            self.address_v6 = Some(address);
+        } else {
+            bail!("duplicate IPv6 address.");
+        }
+        Ok(())
+    }
+
+    fn set_gateway_v6(&mut self, gateway: String) -> Result<(), Error> {
+        if self.gateway_v6.is_none() {
+            self.gateway_v6 = Some(gateway);
+        } else {
+            bail!("duplicate IPv4 gateway.");
+        }
+        Ok(())
+    }
+
+    fn set_netmask_v6(&mut self, mask: u8) -> Result<(), Error> {
+        if self.netmask_v6.is_none() {
+            if mask > 0 && mask <= 128 {
+                self.netmask_v6 = Some(mask);
+            } else {
+                bail!("invalid ipv6 netmaks '{}'", mask);
+            }
+        } else {
+            bail!("duplicate IPv6 netmask.");
+        }
+        Ok(())
+    }
+
+    fn push_addon_option(&mut self, text: String) {
+        match self.address_family {
+            AddressFamily::Inet | AddressFamily::Inet4and6  => self.options_v4.push(text),
+            AddressFamily::Inet6 => self.options_v6.push(text),
+        }
+    }
+
+}
+
+#[derive(Debug)]
+enum NetworkOrderEntry {
+    Iface(String),
+    Comment(String),
+    Option(String),
+}
+
+#[derive(Debug)]
+pub struct NetworkConfig {
+    pub auto_flag: HashSet<String>,
+    interfaces: HashMap<String, Interface>,
+    order: Vec<NetworkOrderEntry>,
+}
+
+impl NetworkConfig {
+
+    pub fn new() -> Self {
+        Self {
+            auto_flag: HashSet::new(),
+            interfaces: HashMap::new(),
+            order: Vec::new(),
+        }
+    }
+
+    pub fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
+
+        fn method_to_str(method: &Option<Token>) -> &str {
+            match method {
+                None => "static",
+                Some(method) => {
+                    match method {
+                        Token::Static => "static",
+                        Token::Loopback => "loopback",
+                        Token::Manual => "manual",
+                        Token::DHCP => "dhcp",
+                        _ => unreachable!(),
+                    }
+                }
+            }
+        }
+
+        fn write_attributes_v4(w: &mut dyn Write, interface: &Interface) -> Result<(), Error> {
+            if let Some(address) = &interface.address_v4 {
+                if let Some(netmask) = interface.netmask_v4 {
+                    writeln!(w, "    address {}/{}", address, netmask)?;
+                } else {
+                    writeln!(w, "    address {}", address)?;
+                }
+            }
+            if let Some(gateway) = &interface.gateway_v4 {
+                writeln!(w, "    gateway {}", gateway)?;
+            }
+            for option in &interface.options_v4 {
+                writeln!(w, "    {}", option)?;
+            }
+
+            Ok(())
+        };
+
+        fn write_attributes_v6(w: &mut dyn Write, interface: &Interface) -> Result<(), Error> {
+            if let Some(address) = &interface.address_v6 {
+                if let Some(netmask) = interface.netmask_v6 {
+                    writeln!(w, "    address {}/{}", address, netmask)?;
+                } else {
+                    writeln!(w, "    address {}", address)?;
+                }
+            }
+            if let Some(gateway) = &interface.gateway_v6 {
+                writeln!(w, "    gateway {}", gateway)?;
+            }
+            for option in &interface.options_v6 {
+                writeln!(w, "    {}", option)?;
+            }
+
+            Ok(())
+        };
+
+        fn write_interface(w: &mut dyn Write, config: &NetworkConfig, name: &str, interface: &Interface) -> Result<(), Error> {
+            if config.auto_flag.contains(name) {
+                writeln!(w, "auto {}", name)?;
+            }
+
+            if interface.address_family == AddressFamily::Inet4and6  && interface.method_v4 == interface.method_v6 {
+                writeln!(w, "iface {} {}", name, method_to_str(&interface.method_v4))?;
+                write_attributes_v4(w, &interface)?;
+                write_attributes_v6(w, &interface)?;
+                writeln!(w)?;
+            } else if interface.address_family == AddressFamily::Inet4and6 {
+                writeln!(w, "iface {} inet {}", name, method_to_str(&interface.method_v4))?;
+                write_attributes_v4(w, &interface)?;
+                writeln!(w)?;
+                writeln!(w, "iface {} inet6 {}", name, method_to_str(&interface.method_v6))?;
+                write_attributes_v6(w, &interface)?;
+                writeln!(w)?;
+            } else if interface.address_family == AddressFamily::Inet  {
+                writeln!(w, "iface {} inet {}", name, method_to_str(&interface.method_v4))?;
+                write_attributes_v4(w, &interface)?;
+                writeln!(w)?;
+            } else if interface.address_family == AddressFamily::Inet6 {
+                writeln!(w, "iface {} inet {}", name, method_to_str(&interface.method_v6))?;
+                write_attributes_v6(w, &interface)?;
+                writeln!(w)?;
+            } else {
+                unreachable!();
+            }
+            Ok(())
+        }
+
+        let mut done = HashSet::new();
+
+        let mut last_entry_was_comment = false;
+
+        for entry in self.order.iter() {
+            match entry {
+                NetworkOrderEntry::Comment(comment) => {
+                    writeln!(w, "#{}", comment)?;
+                    last_entry_was_comment = true;
+                }
+                NetworkOrderEntry::Option(option) => {
+                    if last_entry_was_comment {  writeln!(w)?; }
+                    last_entry_was_comment = false;
+                    writeln!(w, "{}", option)?;
+                    writeln!(w)?;
+                }
+                NetworkOrderEntry::Iface(name) => {
+                    let interface = match self.interfaces.get(name) {
+                        Some(interface) => interface,
+                        None => continue,
+                    };
+
+                    if last_entry_was_comment {  writeln!(w)?; }
+                    last_entry_was_comment = false;
+
+                    if done.contains(name) { continue; }
+                    done.insert(name);
+
+                    write_interface(w, self, name, interface)?;
+                }
+            }
+        }
+
+        for (name, interface) in &self.interfaces {
+            if done.contains(name) { continue; }
+            write_interface(w, self, name, interface)?;
+        }
+        Ok(())
+    }
+}
+
diff --git a/src/config/network/helper.rs b/src/config/network/helper.rs
new file mode 100644 (file)
index 0000000..355f2a1
--- /dev/null
@@ -0,0 +1,48 @@
+use std::collections::HashMap;
+use lazy_static::lazy_static;
+
+pub static IPV4_REVERSE_MASK: &[&'static str] = &[
+    "0.0.0.0",
+    "128.0.0.0",
+    "192.0.0.0",
+    "224.0.0.0",
+    "240.0.0.0",
+    "248.0.0.0",
+    "252.0.0.0",
+    "254.0.0.0",
+    "255.0.0.0",
+    "255.128.0.0",
+    "255.192.0.0",
+    "255.224.0.0",
+    "255.240.0.0",
+    "255.248.0.0",
+    "255.252.0.0",
+    "255.254.0.0",
+    "255.255.0.0",
+    "255.255.128.0",
+    "255.255.192.0",
+    "255.255.224.0",
+    "255.255.240.0",
+    "255.255.248.0",
+    "255.255.252.0",
+    "255.255.254.0",
+    "255.255.255.0",
+    "255.255.255.128",
+    "255.255.255.192",
+    "255.255.255.224",
+    "255.255.255.240",
+    "255.255.255.248",
+    "255.255.255.252",
+    "255.255.255.254",
+    "255.255.255.255",
+];
+
+lazy_static! {
+    pub static ref IPV4_MASK_HASH_LOCALNET: HashMap<&'static str, u8> = {
+        let mut map = HashMap::new();
+        for i in 8..32 {
+            map.insert(IPV4_REVERSE_MASK[i], i as u8);
+        }
+        map
+    };
+}
diff --git a/src/config/network/lexer.rs b/src/config/network/lexer.rs
new file mode 100644 (file)
index 0000000..c897d46
--- /dev/null
@@ -0,0 +1,109 @@
+use std::io::BufRead;
+use std::iter::Iterator;
+use std::collections::{HashMap, VecDeque};
+
+use lazy_static::lazy_static;
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum Token {
+    Text,
+    Comment,
+    DHCP,
+    Newline,
+    Address,
+    Auto,
+    Gateway,
+    Inet,
+    Inet6,
+    Iface,
+    Loopback,
+    Manual,
+    Netmask,
+    Static,
+    Attribute,
+    EOF,
+}
+
+lazy_static! {
+    static ref KEYWORDS: HashMap<&'static str, Token> = {
+        let mut map = HashMap::new();
+        map.insert("address", Token::Address);
+        map.insert("auto", Token::Auto);
+        map.insert("dhcp", Token::DHCP);
+        map.insert("gateway", Token::Gateway);
+        map.insert("inet", Token::Inet);
+        map.insert("inet6", Token::Inet6);
+        map.insert("iface", Token::Iface);
+        map.insert("loopback", Token::Loopback);
+        map.insert("manual", Token::Manual);
+        map.insert("netmask", Token::Netmask);
+        map.insert("static", Token::Static);
+        map
+    };
+}
+
+pub struct Lexer<R> {
+    input: R,
+    eof_count: usize,
+    cur_line: Option<VecDeque<(Token, String)>>,
+}
+
+impl <R: BufRead> Lexer<R> {
+
+    pub fn new(input: R) -> Self {
+        Self { input, eof_count: 0, cur_line: None }
+    }
+
+    fn split_line(line: &str) -> VecDeque<(Token, String)> {
+        if line.starts_with("#") {
+            let mut res = VecDeque::new();
+            res.push_back((Token::Comment, line[1..].trim().to_string()));
+            return res;
+        }
+        let mut list: VecDeque<(Token, String)> = line.split_ascii_whitespace().map(|text| {
+            let token = KEYWORDS.get(text).unwrap_or(&Token::Text);
+            (*token, text.to_string())
+        }).collect();
+
+        if line.starts_with(|c: char| c.is_ascii_whitespace() && c != '\n') {
+            list.push_front((Token::Attribute, String::from("\t")));
+        }
+        list
+    }
+}
+
+impl <R: BufRead> Iterator for Lexer<R> {
+
+    type Item = Result<(Token, String), std::io::Error>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.cur_line.is_none() {
+            let mut line = String::new();
+            match self.input.read_line(&mut line) {
+                Err(err) => return Some(Err(err)),
+                Ok(0) => {
+                    self.eof_count += 1;
+                    if self.eof_count == 1 { return Some(Ok((Token::EOF, String::new()))); }
+                    return None;
+                }
+                _ => {}
+            }
+            self.cur_line = Some(Self::split_line(&line));
+        }
+
+        match self.cur_line {
+            Some(ref mut  cur_line) => {
+                if cur_line.is_empty() {
+                    self.cur_line = None;
+                    return Some(Ok((Token::Newline, String::from("\n"))));
+                } else {
+                    let (token, text) = cur_line.pop_front().unwrap();
+                    return Some(Ok((token, text)));
+                }
+            }
+            None => {
+                return None;
+            }
+        }
+    }
+}
diff --git a/src/config/network/parser.rs b/src/config/network/parser.rs
new file mode 100644 (file)
index 0000000..63c819f
--- /dev/null
@@ -0,0 +1,317 @@
+use std::io::{BufReader};
+use std::fs::File;
+use std::iter::{Peekable, Iterator};
+
+use anyhow::{Error, bail, format_err};
+use lazy_static::lazy_static;
+use regex::Regex;
+
+use proxmox::*; // for IP macros
+
+use super::helper::*;
+use super::lexer::*;
+
+use super::{NetworkConfig, NetworkOrderEntry, Interface, AddressFamily};
+
+pub struct NetworkParser {
+    input: Peekable<Lexer<BufReader<File>>>,
+    line_nr: usize,
+}
+
+impl NetworkParser {
+
+    pub fn new(file: File) -> Self {
+        let reader = BufReader::new(file);
+        let input = Lexer::new(reader).peekable();
+        Self { input, line_nr: 1 }
+    }
+
+    fn peek(&mut self) -> Result<Token, Error> {
+        match self.input.peek() {
+            Some(Err(err)) => {
+                bail!("input error - {}", err);
+            }
+            Some(Ok((token, _))) => {
+                return Ok(*token);
+            }
+            None => {
+                bail!("got unexpected end of stream (inside peek)");
+            }
+        }
+    }
+
+    fn next(&mut self) -> Result<(Token, String), Error> {
+        match self.input.next() {
+            Some(Err(err)) => {
+                bail!("input error - {}", err);
+            }
+            Some(Ok((token, text))) => {
+                if token == Token::Newline { self.line_nr += 1; }
+                return Ok((token, text));
+            }
+            None => {
+                bail!("got unexpected end of stream (inside peek)");
+            }
+        }
+    }
+
+    fn next_text(&mut self) -> Result<String, Error> {
+        match self.next()? {
+            (Token::Text, text) => Ok(text),
+            (unexpected, _) => bail!("got unexpected token {:?} (expecting Text)", unexpected),
+        }
+    }
+
+    fn eat(&mut self, expected: Token) -> Result<(), Error> {
+        let (next, _) = self.next()?;
+        if next != expected {
+            bail!("expected {:?}, got {:?}", expected, next);
+        }
+        Ok(())
+    }
+
+    fn parse_auto(&mut self, config: &mut NetworkConfig) -> Result<(), Error> {
+        self.eat(Token::Auto)?;
+
+        let iface = self.next_text()?;
+        println!("AUTO {}", iface);
+
+        self.eat(Token::Newline)?;
+
+        config.auto_flag.insert(iface.to_string());
+
+        Ok(())
+    }
+
+    fn parse_iface_address(&mut self, interface: &mut Interface) -> Result<(), Error> {
+        self.eat(Token::Address)?;
+        let address = self.next_text()?;
+
+        lazy_static! {
+            pub static ref ADDRESS_V4_REGEX: Regex = Regex::new(
+                concat!(r"^(", IPV4RE!(), r")(?:/(\d{1,2}))?$")
+            ).unwrap();
+           pub static ref ADDRESS_V6_REGEX: Regex = Regex::new(
+               concat!(r"^(", IPV6RE!(), r")(?:/(\d{1,2}))?$")
+            ).unwrap();
+        }
+
+        if let Some(caps) = ADDRESS_V4_REGEX.captures(&address) {
+            let address = caps.get(1).unwrap().as_str();
+            interface.set_address_v4(address.to_string())?;
+            if let Some(mask) = caps.get(2) {
+                let mask = u8::from_str_radix(mask.as_str(), 10)?;
+                interface.set_netmask_v4(mask)?;
+            }
+        } else if let Some(caps) = ADDRESS_V6_REGEX.captures(&address) {
+            let address = caps.get(1).unwrap().as_str();
+            interface.set_address_v6(address.to_string())?;
+            if let Some(mask) = caps.get(2) {
+                let mask = u8::from_str_radix(mask.as_str(), 10)?;
+                interface.set_netmask_v6(mask)?;
+            }
+        } else {
+             bail!("unable to parse IP address");
+        }
+
+        self.eat(Token::Newline)?;
+
+        Ok(())
+    }
+
+    fn parse_iface_gateway(&mut self, interface: &mut Interface) -> Result<(), Error> {
+        self.eat(Token::Gateway)?;
+        let gateway = self.next_text()?;
+
+        if proxmox::tools::common_regex::IP_REGEX.is_match(&gateway) {
+            if gateway.contains(':') {
+                interface.set_gateway_v6(gateway)?;
+            } else {
+                interface.set_gateway_v4(gateway)?;
+            }
+        } else {
+            bail!("unable to parse gateway address");
+        }
+
+        self.eat(Token::Newline)?;
+
+        Ok(())
+    }
+
+    fn parse_to_eol(&mut self) -> Result<String, Error> {
+        let mut line = String::new();
+        loop {
+            match self.next()? {
+                (Token::Newline, _) => return Ok(line),
+                (_, text) => {
+                    if !line.is_empty() { line.push(' '); }
+                    line.push_str(&text);
+                }
+            }
+        }
+    }
+
+    fn parse_iface_addon_attribute(&mut self, interface: &mut Interface) -> Result<(), Error> {
+        let option = self.parse_to_eol()?;
+        if !option.is_empty() { interface.push_addon_option(option) };
+        Ok(())
+    }
+
+    fn parse_iface_netmask(&mut self, interface: &mut Interface) -> Result<(), Error> {
+        self.eat(Token::Netmask)?;
+        let netmask = self.next_text()?;
+
+        if let Some(mask) = IPV4_MASK_HASH_LOCALNET.get(netmask.as_str())  {
+            interface.set_netmask_v4(*mask)?;
+        } else {
+            match u8::from_str_radix(netmask.as_str(), 10) {
+                Ok(mask) => {
+                    if mask <= 32 { interface.set_netmask_v4(mask)?; }
+                    interface.set_netmask_v6(mask)?;
+                }
+                Err(err) => {
+                    bail!("unable to parse netmask '{}' - {}", netmask, err);
+                }
+            }
+        }
+
+        self.eat(Token::Newline)?;
+
+        Ok(())
+    }
+
+    fn parse_iface_attributes(&mut self, interface: &mut Interface) -> Result<(), Error> {
+
+        loop {
+            match self.peek()? {
+                Token::Attribute => self.eat(Token::Attribute)?,
+                Token::Newline => break,
+                unexpected => bail!("unknown token {:?}", unexpected),
+            }
+
+            match self.peek()? {
+                Token::Address => self.parse_iface_address(interface)?,
+                Token::Gateway => self.parse_iface_gateway(interface)?,
+                Token::Netmask => self.parse_iface_netmask(interface)?,
+                _ => {
+                    self.parse_iface_addon_attribute(interface)?;
+                },
+            }
+        }
+
+        Ok(())
+    }
+
+    fn parse_iface(&mut self, config: &mut NetworkConfig) -> Result<(), Error> {
+        self.eat(Token::Iface)?;
+        let iface = self.next_text()?;
+
+        let mut address_family = None;
+        let mut config_method = None;
+
+        loop {
+            let (token, text) = self.next()?;
+            match token {
+                Token::Newline => break,
+                Token::Inet => {
+                    address_family = Some(match address_family {
+                        None => AddressFamily::Inet,
+                        Some(AddressFamily::Inet) => AddressFamily::Inet,
+                        Some(AddressFamily::Inet6) => AddressFamily::Inet4and6,
+                        Some(AddressFamily::Inet4and6) => AddressFamily::Inet4and6,
+                    });
+                }
+                Token::Inet6 => {
+                    address_family = Some(match address_family {
+                        None => AddressFamily::Inet6,
+                        Some(AddressFamily::Inet) => AddressFamily::Inet4and6,
+                        Some(AddressFamily::Inet6) => AddressFamily::Inet6,
+                        Some(AddressFamily::Inet4and6) => AddressFamily::Inet4and6,
+                    });
+                }
+                Token::Loopback | Token::Static | Token::Manual | Token::DHCP => {
+                    if config_method.is_none() {
+                        config_method = Some(token);
+                    } else {
+                        bail!("multiple configuration method definitions");
+                    }
+                }
+                _ => bail!("unknown iface option {}", text),
+            }
+        }
+
+        let address_family = address_family.unwrap_or(AddressFamily::Inet4and6);
+
+        let has_attributes = self.peek()? == Token::Attribute;
+
+        if let Some(mut interface) = config.interfaces.get_mut(&iface) {
+            let compatible = match interface.address_family {
+                AddressFamily::Inet => {
+                    interface.method_v6 = config_method;
+                    address_family == AddressFamily::Inet6
+                }
+                AddressFamily::Inet6 => {
+                    interface.method_v4 = config_method;
+                    address_family == AddressFamily::Inet
+                }
+                _ => false,
+            };
+            if !compatible {
+                bail!("duplicate config for iface '{}'", iface);
+            }
+            interface.address_family = AddressFamily::Inet4and6;
+            if has_attributes { self.parse_iface_attributes(&mut interface)?; }
+        } else {
+            let mut interface = Interface::new(iface.clone(), address_family, config_method);
+
+            if has_attributes { self.parse_iface_attributes(&mut interface)?; }
+
+            config.interfaces.insert(interface.name.clone(), interface);
+            config.order.push(NetworkOrderEntry::Iface(iface));
+        }
+
+        Ok(())
+    }
+
+    pub fn parse_interfaces(&mut self) -> Result<NetworkConfig, Error> {
+        self._parse_interfaces()
+            .map_err(|err| format_err!("line {}: {}", self.line_nr, err))
+    }
+
+    pub fn _parse_interfaces(&mut self) -> Result<NetworkConfig, Error> {
+        let mut config = NetworkConfig::new();
+
+        loop {
+            let peek = self.peek()?;
+            println!("TOKEN: {:?}", peek);
+            match peek {
+                Token::EOF => {
+                    // fixme: trailing comments
+                    return Ok(config);
+                }
+                Token::Newline => {
+                    self.eat(Token::Newline)?;
+                    // fixme end of entry
+                }
+                Token::Comment => {
+                    let (_, text) = self.next()?;
+                    println!("COMMENT: {}", text);
+                    config.order.push(NetworkOrderEntry::Comment(text));
+                    self.eat(Token::Newline)?;
+                }
+                Token::Auto => {
+                    self.parse_auto(&mut config)?;
+                }
+                Token::Iface => {
+                    self.parse_iface(&mut config)?;
+                }
+                _ => {
+                    let option = self.parse_to_eol()?;
+                    if !option.is_empty() {
+                        config.order.push(NetworkOrderEntry::Option(option));
+                    }
+                }
+            }
+        }
+    }
+}