Просто много аплинков
Уличная магия
Уличная магия она такая магия, и такая вся уличная

Пс, посоны, хотите немного настоящей уличной магии? Как бы мультилинком на разделённых таблицах маршрутизации никого не удивишь. Это даже в документации есть, но всё равно иногда кто-нибудь да запилит очередную статью об том, как это делать (в 2015ом, Карл, какой-то «серьёзной» компанией). А я вам покажу, как всё тоже самое сделать, если у вас адресация прилетает по dhcp от провайдеров!

Короче, у меня тут зеркало debian дома есть (что? нафига? так надо!), и захотел я, чтобы оно синкалось не через основной линк. И ради этого я позвонил старому провайдеру и оживил второй линк, висевший без дела со времён семейной жизни в Ясенево. Кстати, он уже разок пригодился как основной, так как свитч основного провайдера, что находится в подвале, всплыл в начале отопительного сезона. А надо было срочно сервер, ssh, чинить-чинить удалённо. Ну как обычно, в общем. И подумал я, что стоит завести их (линки) в один роутер уже. Но они с dhcp. Нужен был dhcp-клиент, чтобы умел в разные таблицы маршрутизации писать параметры для разных линков. Это не про isc-dhcpc конечно…

Но оказалось, что udhcpc для применения конфигурации дёргает внешний файл. Им может быть или скомпилированный бинарник, или скрипт на ЛЮБОМ ЯП, лишь бы запускался в системе. Ну а запустить udhcpc с нужными параметрами при помощи строчек up/down в конфиге интерфейса — вообще не вопрос. Начнём со скрипта:

aliech@fomalhaut:~$ cat /etc/udhcpc/udhcpc-script.pl 
#!/usr/bin/perl
use strict;
use warnings;
use Fcntl qw(:flock);
use NetAddr::IP;
use Sys::Syslog;


openlog('udhcpc-script', 'cons,pid', 'local7');

# Читаем конфигурацию и забираем аргументы
# из строки запуска и переменных окружения.
my $tables = read_conf_file("/etc/udhcpc/udhcpc-script.conf") or sysdie("Can't read configuration file!\n");

my $request = {
	'type'		=> undef,
	'iface'		=> undef,
	'addr'		=> undef,
	'mask'		=> undef,
	'bcast'		=> undef,
	'mtu'		=> undef,
	'router'	=> undef,
	'table'		=> undef
};

sysdie("You must supply request type in command line!\n") unless($#ARGV > -1);
sysdie("Valid requests type are 'deconfig', 'bound', 'renew', 'nak' and 'leasefail'!\n")
unless($ARGV[0] =~ /^(deconfig|bound|renew|nak|leasefail)$/s);
$request->{'type'} = shift(@ARGV);

sysdie("\$ENV{'interface'} isn't set!\n") unless(exists($ENV{'interface'}));
$request->{'iface'} = $ENV{'interface'};
$request->{'table'} = search_table($tables, $request->{'iface'})
or sysdie("Can't find table name by supplied interface name (" . $request->{'iface'} . ")!\n");

$request->{'addr'} = $ENV{'ip'} if(exists($ENV{'ip'}));
$request->{'mask'} = $ENV{'subnet'} if(exists($ENV{'subnet'}));
$request->{'bcast'} = $ENV{'broadcast'} if(exists($ENV{'broadcast'}));
$request->{'mtu'} = $ENV{'mtu'} if(exists($ENV{'mtu'}));
$request->{'router'} = $ENV{'router'} if(exists($ENV{'router'}));

if($request->{'type'} =~ /^(bound|renew)$/) {
	sysdie("Address and mask must be supply with selected request type!\n")
	unless($request->{'addr'} and $request->{'mask'});
}

# Отработка запросов, предварительно забрав текующую конфигурацию.
my $cur_iface = get_iface_conf($request)
or sysdie("Can't get current iface (" . $request->{'iface'} . ") configuration!\n");

if($request->{'type'} eq "deconfig") {
	sysprint("'deconfig' called on " . $request->{'iface'} . "\n");
	deconfig_it($cur_iface, $request, $tables)
	or sysdie("Can't perform interface (" . $request->{'iface'} . ") deconfiguration!\n");
}
elsif($request->{'type'} eq "bound") {
	sysprint("'bound' called on " . $request->{'iface'} . "\n");
	bound_it($request, $tables)
	or sysdie("Can't perform interface (" . $request->{'iface'} . ") configuration!\n");
}
elsif($request->{'type'} eq "renew") {
	sysprint("'renew' called on " . $request->{'iface'} . "\n");

	my $need_renew = 0;

	if($request->{'mtu'}) {
		$need_renew = 1 unless($request->{'mtu'} == $cur_iface->{'mtu'});
	}

	$need_renew = 1 unless($request->{'addr'} eq $cur_iface->{'addr'});
	$need_renew = 1 unless($request->{'mask'} eq $cur_iface->{'mask'});

	my $old_bcast = "";
	$old_bcast = $cur_iface->{'bcast'} if($cur_iface->{'bcast'});
	my $new_bcast = "";
	$new_bcast = $request->{'bcast'} if($request->{'bcast'});
	if($old_bcast ne $new_bcast) {
		$need_renew = 1;
	}

	my $old_router = "";
	$old_router = $cur_iface->{'router'} if($cur_iface->{'router'});
	my $new_router = "";
	$new_router = $request->{'router'} if($request->{'router'});
	if($old_router ne $new_router) {
		$need_renew = 1;
	}

	if($need_renew) {
		sysprint("'renew' performed on " . $request->{'iface'} . "\n");
		deconfig_it($cur_iface, $request, $tables)
		or sysdie("Can't perform interface (" . $request->{'iface'} . ") deconfiguration (renew)!\n");
		bound_it($request, $tables)
		or sysdie("Can't perform interface (" . $request->{'iface'} . ") configuration (renew)!\n");
	}
}
else {
	sysprint("Requested '" . $request->{'type'} . "' action!\n"); 
} 


closelog();
exit(0);


# Функция чтения конфигурационного файла.
# Конфигурация составляется в формате 'имя_таблицы имя_интерфейса 0_или_1'.
# Коментарии допустимы)
# Открываем файл, ставим блокировку, читаем строчку за строчкой,
# удаляя пустые строки, отступы и коментарии.
# Всё, что в строке останется, будет поделено на три части
# и уложено в соответствующий хеш.
sub read_conf_file {
	my $path = shift();

	my %tables = ();

	my $fh;
	unless(open($fh, "<", $path)) {
		syswarn("Can't open configuration file " . $path . ": " . $! . "\n");
		return(undef);
	}

	unless(flock($fh, LOCK_SH)) {
		syswarn("Can't set sahred lock on configuration file " . $path . ": " . $! . "\n");
		close($fh);
		return(undef);
	}

	while(my $line = <$fh>) {
		chomp($line);
		$line =~ s/#.*//g;
 		$line =~ s/^\s*//g;
		$line =~ s/\s*$//g;
		next() unless(length($line));

		my @parts = split(/\s+/, $line);
		if($#parts != 2) {
			syswarn("Can't read configuration file " . $path . ": syntax error!\n");
			close($fh);
			return(undef);
		}

		$tables{$parts[0]} = {
			'iface'		=> $parts[1],
			'default'	=> $parts[2]
		};
	}

	close($fh);

	return(\%tables);
}


# Функция, собирающая информацию об интерфейсе.
# Нужно собрать mtu, адресс, маску и бродкаст.
# Если строки из вывода не влазят в шаблон, то считаем,
# что нет возможности собрать требуемые сведения.
sub get_iface_conf {
	my $request = shift();

	my %ifconf = (
		'addr'	=> undef,
		'mask'	=> undef,
		'bcast'	=> undef,
		'mtu'	=> undef,
		'router'=> undef
	);

	my $fh;
	my $line;

	unless(open($fh, "-|", 'ip', '-o', 'link', 'show', $request->{'iface'})) {
		syswarn("Can't execute 'ip -o link show " . $request->{'iface'} . "'!\n");
		return(undef);
	}

	$line = <$fh>;
	close($fh);

	if($line) {
		if($line =~ /\smtu\s+(\d+)\s/) {
			$ifconf{'mtu'} = $1;
		}
	}

	unless(open($fh, "-|", 'ip', '-o', '-4', 'address', 'show', $request->{'iface'})) {
		syswarn("Can't execute 'ip -o -4 address show " . $request->{'iface'} . "'!\n");
		return(undef);
	}

	$line = <$fh>;
	close($fh);

	if($line) {
		if($line =~ /\sinet\s+(\S+)\s/) {
			my $ip_obj = NetAddr::IP->new($1);
			$ifconf{'addr'} = $ip_obj->addr();
			$ifconf{'mask'} = $ip_obj->mask();
		}

		if($line =~ /\sbrd\s+(\S+)\s/) {
			$ifconf{'bcast'} = $1;
		}
	}

	unless(open($fh, "-|", 'ip', '-4', 'route', 'list', 'table', $request->{'table'})) {
		syswarn("Can't execute 'ip -4 route list table " . $request->{'table'} . "': " . $! . "\n");
		return(undef);
	}

	while(my $line = <$fh>) {
		if($line =~ /(?:^|\s)default\s+via\s+(\S+)(?:\s|$)/) {
			$ifconf{'router'} = $1;
		}
	}
	close($fh);

	return(\%ifconf);
}


# Функция поиска роутов. Ищет для интерфеса по вхождению имени интерфейса,
# локальной подсети на интрфейсе и по адресу интерфейса. Ищет для указанных
# таблиц и таблицы main.
sub search_routes {
	my $ifname = shift();
	my $ifconf = shift();
	my $tables = shift();

	my @routes = ();

	my $target_net = "THIS_IS_EMPTY_STRING";
	my $target_src = "THIS_IS_EMPTY_STRING";
	if($ifconf->{'addr'}) {
		my $ip_obj = NetAddr::IP->new($ifconf->{'addr'}, $ifconf->{'mask'});
		$target_net = $ip_obj->network();
		$target_src = $ifconf->{'addr'};
	}

	foreach my $table (keys(%{$tables}), 'main') {
		my $fh;
		unless(open($fh, "-|", "ip", "route", "list", "table", $table)) {
			syswarn("Can't execute 'ip route list table " . $table . "'!\n");
			return(undef);
		}

		while(my $line = <$fh>) {
			$line =~ s/\r?\n$//;
			$line =~ s/^\s*//g;
			$line =~ s/\s*$//g;

			if(
				($line =~ /^$target_net\s/) or
				($line =~ /\sdev\s+$ifname(\s|$)/) or
				($line =~ /\ssrc\s+$target_src$/)
			) {
				push(@routes, $line . " table " . $table);
			}
		}

		close($fh);
	}

	return(\@routes);
}


# Функция разбиения строки в массив по пробелам.
sub chop_chop {
	my $string = shift();
	my @parts = split(/\s+/, $string);
	return(@parts);
}


# Поиск имени таблицы по привязанному интерфейсу.
sub search_table {
	my $tables = shift();
	my $iface = shift();

	my $table = undef;

	foreach my $name (keys(%{$tables})) {
		if($tables->{$name}->{'iface'} eq $iface) {
			$table = $name;
			last();
		}
	}

	return($table);
}


# Так как deconf нужен много где, мы выносим его в функцию.
sub deconfig_it {
	my $cur_iface = shift();
	my $request = shift();
	my $tables = shift();

	if($cur_iface->{'addr'}) {
		if(system(
			'ip',
			'rule',
			'del',
			'from',
			$cur_iface->{'addr'} . '/32',
			'table',
			$request->{'table'}
		)) {
			syswarn("Can't execute 'ip rule del from " . $cur_iface->{'addr'} . "/32 table " . $request->{'table'} . "'!\n");
			return(undef);
		}
	}

	if(system(
		'ip',
		'address',
		'flush',
		'dev',
		$request->{'iface'}
	)) {
		syswarn("Can't execute 'ip address flush dev " . $request->{'iface'} . "'!\n");
		return(undef);
	}

	if(system(
		'ip',
		'link',
		'set',
		$request->{'iface'},
		'up'
	)) {
		syswarn("Can't execute 'ip link set " . $request->{'iface'} . " up'!\n");
		return(undef);
	}

	my $last_routes = search_routes($request->{'iface'}, $cur_iface, $tables) or return(undef);
	foreach my $route (@{$last_routes}) {
		if(system(
				'ip',
				'route',
				'del',
				chop_chop($route)
		)) {
			syswarn("Can't execute 'ip route del " . $route . "'!\n");
			return(undef);
		}
	}

	return(1);
}


# bound тоже применяется два раза, - выносим сюда
sub bound_it {
	my $request = shift();
	my $tables = shift();

	if($request->{'mtu'}) {
		if(system(
			'ip',
			'link',
			'set',
			$request->{'iface'},
			'mtu',
			$request->{'mtu'}
		)) {
			syswarn("Can't execute 'ip link set " . $request->{'iface'} . " mtu " . $request->{'mtu'} . "'!\n");
			return(undef);
		}
	}

	my @ip_addr_cmd = (
		'ip',
		'address',
		'add',
		$request->{'addr'} . '/' . $request->{'mask'}
	);

	if($request->{'bcast'}) {
		push(@ip_addr_cmd, (
			'broadcast',
			$request->{'bcast'}
		));
	}

	push(@ip_addr_cmd, (
		'dev',
		$request->{'iface'}
	));

	if(system(@ip_addr_cmd)) {
		syswarn("Can't execute '" . uncut_list(@ip_addr_cmd) . "'!\n");
		return(undef);
	}

	my $ip_obj = NetAddr::IP->new($request->{'addr'}, $request->{'mask'});
	foreach my $table (keys(%{$tables})) {
		if(system(
			'ip',
			'route',
			'add',
			$ip_obj->network(),
			'dev',
			$request->{'iface'},
			'proto',
			'static',
			'scope',
			'link',
			'src',
			$request->{'addr'},
			'table',
			$table
		)) {
			syswarn("Can't execute 'ip route add " . $ip_obj->network() . " dev " . $request->{'iface'} .
			" proto static scope link src " . $request->{'addr'} . " table " . $request->{'table'} . "'!\n");
			return(undef);
		}

		if(($table eq $request->{'table'}) and ($request->{'router'})) {
			system(
				'ip',
				'route',
				'add',
				'default',
				'via',
				$request->{'router'},
				'dev',
				$request->{'iface'},
				'table',
				$request->{'table'}
			);
		}
	}

	if(system(
		'ip',
		'rule',
		'add',
		'from',
		$request->{'addr'} . '/32',
		'table',
		$request->{'table'}
	)) {
		syswarn("Can't execute 'ip rule add from " . $request->{'addr'} . "/32 table " . $request->{'table'} . "'!\n");
		return(undef);
	}

	if(system(
		'ip',
		'-6',
		'address',
		'flush',
		'dev',
		$request->{'iface'}
	)) {
		syswarn("Can't execute 'ip -6 address flush dev " . $request->{'iface'} . "'!\n");
		return(undef);
	}

	if($tables->{$request->{'table'}}->{'default'} > 0) {
		my $fh;
		my $cur_router = "";

		unless(open($fh, "-|", 'ip', '-4', 'route', 'list', 'table', 'main')) {
			syswarn("Can't execute 'ip -4 route list table main': " . $! . "\n");
			return(undef);
		}

		while(my $line = <$fh>) {
			if($line =~ /(?:^|\s)default\s+via\s+(\S+)(?:\s|$)/) {
				$cur_router = $1;
			}
		}
		close($fh);

		my $new_router = "";
		$new_router = $request->{'router'} if($request->{'router'});
		if(($cur_router ne "") and ($cur_router ne $new_router)) {
			if(system(
				'ip',
				'-4',
				'route',
				'delete',
				'default'
			)) {
				syswarn("Can't execute 'ip -4 route delete default'!\n");
				return(undef);
			}
		}

		if($request->{'router'}) {
			if(system(
				'ip',
				'route',
				'add',
				'default',
				'via',
				$request->{'router'},
				'dev',
				$request->{'iface'}
			)) {
				syswarn("Can't execute 'ip route add default via " . $request->{'router'} . " dev " . $request->{'iface'} . "'!\n");
				return(undef);
			}
		}
	}

	return(1);
}


# ещё одна служебная функция
sub uncut_list {
	my @parts = @_;

	my $string = "";

	foreach my $part (@parts) {
		if(lenght($string)) {
			$string .= " ";
		}

		$string .= $part;
	}

	return($string);
}


sub sysdie {
	my $msg = shift();
	chomp($msg);
	syslog('error', $msg);
	closelog();
	exit(1);

	return(1);
}


sub syswarn {
	my $msg = shift();
	chomp($msg);
	syslog('warning', $msg);

	return(1);
}


sub sysprint {
	my $msg = shift();
	chomp($msg);
	syslog('info', $msg);

	return(1);
}
aliech@fomalhaut:~$ cat /etc/udhcpc/udhcpc-script.conf 
onlime vlan999 1
netbynet vlan998 0
aliech@fomalhaut:~$ 

И конфиги аплинков:

aliech@fomalhaut:~$ cat /etc/network/interfaces.d/vlan998
auto vlan998
iface vlan998 inet manual
	pre-up		ip link show eth0 | grep -q DOWN && ip link set eth0 up || true
	pre-up		ip link add link eth0 name vlan998 type vlan id 998 gvrp off
	pre-up		ip link set vlan998 up
	up		udhcpc -s /etc/udhcpc/udhcpc-script.pl -p /var/run/udhcpc.vlan998.pid -i vlan998
	down		kill -USR2 `cat /var/run/udhcpc.vlan998.pid` && sleep 5
	down		kill `cat /var/run/udhcpc.vlan998.pid`
	post-down	ip link set vlan998 down
	post-down	ip link del vlan998
aliech@fomalhaut:~$ cat /etc/network/interfaces.d/vlan999
auto vlan999
iface vlan999 inet manual
	pre-up		ip link show eth0 | grep -q DOWN && ip link set eth0 up || true
	pre-up		ip link add link eth0 name vlan999 type vlan id 999 gvrp off
	pre-up		ip link set vlan999 up
	up		udhcpc -s /etc/udhcpc/udhcpc-script.pl -p /var/run/udhcpc.vlan999.pid -i vlan999
	down		sleep 10 && kill -USR2 `cat /var/run/udhcpc.vlan999.pid`
	down		kill `cat /var/run/udhcpc.vlan999.pid`
	post-down	ip link set vlan999 down
	post-down	ip link del vlan999
aliech@fomalhaut:~$

Ну как-то так. Работает. Два месяца уже как. Ни разу скрипт не на лажал) А вообще, я очень благодарен создателям busybox и udhcpc, — они создали отличные и простые инструменты, которые, использую изоленту и поминая чью-то матерь, можно очень хорошо встроить куда-либо. Я ведь был близок к тому, чтобы пойти уже и написать свой собственный клиент. Но обошлось! И это великолепно, так как сэкономленное время я потратил на сон на другие бесполезные вещи.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

5 × = 15