diff options
| author | Michael Biebl <biebl@debian.org> | 2024-05-05 00:07:30 +0200 |
|---|---|---|
| committer | Michael Biebl <biebl@debian.org> | 2024-05-05 00:07:30 +0200 |
| commit | 34bb501be08aa2b313d88e67d6e0a7e0a3f9cfa6 (patch) | |
| tree | 4e6220877828be4c6f261de09ec0cb2d80e32389 /contrib/scripts | |
| parent | bba2e4b4de668db525cbfdfc35292e5a0b51671a (diff) | |
New upstream version 1.47.90 upstream/1.47.90
Diffstat (limited to 'contrib/scripts')
26 files changed, 4584 insertions, 0 deletions
diff --git a/contrib/scripts/NM-log b/contrib/scripts/NM-log new file mode 100755 index 00000000..85faea86 --- /dev/null +++ b/contrib/scripts/NM-log @@ -0,0 +1,85 @@ +#!/bin/bash + +# Util to pretty-print logfile of NetworkManager +# +# Unless setting NM_LOG_NO_COLOR it will colorize the output. +# Suppress coloring with: +# $ NM_LOG_NO_COLOR=1 NM-log ... +# +# If called without arguments, it either reads from stdin (if not +# connected to a terminal) or it shows the journal content. +# +# If called with first argument "j", it always shows the journal content. +# +# You can pass multiple filenames. + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + NM_not_sourced=1 +else + unset NM_not_sourced +fi + +NM-show-journal() { + local since="$(systemctl show NetworkManager | sed -n 's/^ExecMainStartTimestamp=\(.*\) [A-Z0-9]\+$/\1/p')" + + if [[ "$since" == "" ]]; then + echo "error detecting NM. Is it running?" + systemctl status NetworkManager + else + journalctl -o short-precise --since "$since" -b 0 -u NetworkManager "$@" + fi +} + +NM-colorize() { + if [[ "$NM_LOG_NO_COLOR" == "" ]]; then + # poor man's coloring using grep. + # TODO: do it somehow better (and more efficient). + sed 's/\r$//' | \ + GREP_COLOR='01;31' grep -a --color=always '^\|^\(.* \)\?<\(warn> \|error>\) \[[0-9.]*\]' | \ + GREP_COLOR='01;33' grep -a --color=always '^\|^\(.* \)\?<info> \[[0-9.]*\]\( .*\<is starting\>.*$\)\?' | \ + GREP_COLOR='01;37' grep -a --color=always '^\|\<platform:\( (.*)\)\? signal: .*$' | \ + GREP_COLOR='01;34' grep -a --color=always '^\|\<platform\(-linux\)\?:\( (.*)\)\? link: \(add\|adding\|change\|setting\|deleting\|enslaving to master\|releasing \([0-9]\+ \)\?from master\)\>\|\<platform: routing-rule: \(adding or updating:\|delete \)\|\<platform:\( (.*)\)\? address: \(deleting\|adding or updating\) IPv. address:\? \|\<platform:\( (.*)\)\? \(route\|ip4-route\|ip6-route\|qdisc\|tfilter\): \([a-z]\+\|adding or updating\|new\[0x[0-9A-Za-z]*\]\) \|\<platform-linux: sysctl: setting ' | \ + GREP_COLOR='01;35' grep -a --color=always '^\|\<audit: .*$' | \ + GREP_COLOR='01;32' grep -a --color=always '^\|\<device (.*): state change: ' | + if [[ "$NM_LOG_GREP" != "" ]]; then + GREP_COLOR='01;36' grep -a --color=always "^\\|$NM_LOG_GREP" + else + /bin/cat - + fi + else + /bin/cat - + fi +} + +NM-log() { + local NM_LOG_GREP= + + while [[ $# -gt 0 ]]; do + if [[ "$1" == "-h" ]]; then + shift + NM_LOG_GREP="${NM_LOG_GREP+$NM_LOG_GREP\\|}\\<$1\\>" + shift + else + break + fi + done + + ( + if [ "$1" == "j" ]; then + shift + NM-show-journal "$@" + elif [ "$#" -eq 0 -a -t 0 ]; then + NM-show-journal + else + a="${1--}" + shift + /usr/bin/less -f "$a" "$@" + fi + ) | \ + NM_LOG_GREP="$NM_LOG_GREP" NM-colorize | \ + LESS=FRSXM less -f -R --shift=5 +} + +if [[ "$NM_not_sourced" != "" ]]; then + NM-log "$@" +fi diff --git a/contrib/scripts/anonymize-logs.py b/contrib/scripts/anonymize-logs.py new file mode 100755 index 00000000..36b82ed1 --- /dev/null +++ b/contrib/scripts/anonymize-logs.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 + +from textwrap import wrap +import subprocess +import ipaddress +import argparse +import os +import re + + +domains = [] + +hosts_sub = {} +host_next = 0 + +macs_sub = {} +mac_next = 0 + +ips_sub = {} +ip4_next = ipaddress.IPv4Address("0.0.0.0") +ip6_next = ipaddress.IPv6Address("ffff::") + + +def main(args): + must_autoreplace_hostnames = not args.show_hostnames + must_replace_hostnames = must_autoreplace_hostnames or args.domain or args.hostname + + init_hostnames_and_domains_sub(args) + + with open(args.log_file) as f: + for line in (line.strip() for line in f): + if must_replace_hostnames: + line = replace_hostnames(line, must_autoreplace_hostnames) + if not args.show_macs: + line = replace_macs(line) + if not args.show_public_ips or args.hide_private_ips: + line = replace_ips(line, args.show_public_ips, args.hide_private_ips) + + print(line) + + +def init_hostnames_and_domains_sub(args): + global domains + + if not args.show_hostnames: + domains.extend(["com", "org", "net", "gov", "es", "it"]) + + r = subprocess.run("hostname", capture_output=True) + if r.returncode == 0: + own_hostname = r.stdout.decode().strip() + add_host_sub(own_hostname, ".self") + + # domains and hostname passed explicitly are replaced even with --show-hostnames + domains.extend(d.strip(". ") for d in args.domain) + domains = "|".join(domains) + + for hostname in args.hostname: + add_host_sub(hostname) + + +def add_host_sub(hostname: str, suffix: str = ""): + global hosts_sub + global host_next + + # if it's a domain-like hostname (i.e example.com) adds .ext at the end + if suffix == "" and re.search(r"\.({})$".format(domains), hostname): + suffix = ".ext" + + if hostname not in hosts_sub: + hosts_sub[hostname] = "hostname{}{}".format(host_next, suffix) + host_next += 1 + + +def replace_hostnames(line: str, autodetect_from_logs: bool) -> str: + global hosts_sub + + # look for known log messages that show hostnames + if autodetect_from_logs: + match = re.search(r"get-hostname: \"(.*)\"", line) + if match: + add_host_sub(match.group(1)) + + match = re.search(r"set hostname to \"(.*)\"", line) + if match: + add_host_sub(match.group(1)) + + match = re.search( + r"hostname changed from (\(none\)|\".*\") to (\(none\)|\".*\")", line + ) + if match: + if match.group(1) != "(none)": + add_host_sub(match.group(1).strip('"')) + if match.group(2) != "(none)": + add_host_sub(match.group(2).strip('"')) + + # look for domain-like strings + if domains: + match = re.search(r"[\w\-\.]+?\.(" + domains + r")\b", line) + if match: + add_host_sub(match.group(0)) + + for orig, repl in hosts_sub.items(): + line = line.replace(orig, repl) + + return line + + +def replace_macs(line: str) -> str: + global macs_sub + global mac_next + + macs = re.findall(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", line) + + for mac in macs: + if mac not in macs_sub: + macs_sub[mac] = ":".join(wrap("{:012x}".format(mac_next), width=2)) + mac_next += 1 + + line = line.replace(mac, macs_sub[mac]) + + return line + + +def replace_ips(line: str, show_public: bool, hide_private: bool) -> str: + global ips_sub + global ip4_next + global ip6_next + + ips4 = re.findall(r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}", line) + ips6 = re.findall(r"(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}", line) + + for addr_str in ips4 + ips6: + try: + addr = ipaddress.ip_address(addr_str) + except: # not IP + continue + + if (addr.is_private and not hide_private) or (addr.is_global and show_public): + continue + + if addr.exploded not in ips_sub: + if type(addr) is ipaddress.IPv4Address: + ips_sub[addr.exploded] = str(ip4_next).replace("0.", "IP4.", 1) + ip4_next += 1 + else: + ips_sub[addr.exploded] = str(ip6_next).replace("ffff:", "IPv6:", 1) + ip6_next += 1 + + line = line.replace(addr_str, ips_sub[addr.exploded]) + + return line + + +if __name__ == "__main__": + args_parser = argparse.ArgumentParser( + prog=os.path.basename(__file__), + description="""Anonymize some data from NetworkManager logs. + +Note that it only covers some common stuff like MAC and IP addresses or +hostnames. Do not trust it and manually review that the log doesn't contain +sensitive data before sharing it. + +Changing IP address can make that problems related to routing are impossible to +analyze. Because of that, private IPs which are normally not sensitive are not +hidden by default, and if the problem is related to routing you might need to +use the --show-public-ips option""", + epilog="Options of the type --show-* disable masking that type of data.", + formatter_class=argparse.RawTextHelpFormatter, + ) + args_parser.add_argument("-H", "--show-hostnames", action="store_true") + args_parser.add_argument("-m", "--show-macs", action="store_true") + args_parser.add_argument("-g", "--show-public-ips", action="store_true") + args_parser.add_argument("-p", "--hide-private-ips", action="store_true") + args_parser.add_argument( + "-d", + "--domain", + action="append", + default=[], + help='additional domains to hide, like ".xyz", can be passed more than once', + ) + args_parser.add_argument( + "-n", + "--hostname", + action="append", + default=[], + help="additional hostnames to hide, can be passed more than once", + ) + args_parser.add_argument( + "log_file", nargs="?", default="/dev/stdin", help="Log file (by default, stdin)" + ) + + args = args_parser.parse_args() + main(args) diff --git a/contrib/scripts/btmodem.pl b/contrib/scripts/btmodem.pl new file mode 100755 index 00000000..feaa32e0 --- /dev/null +++ b/contrib/scripts/btmodem.pl @@ -0,0 +1,291 @@ +#!/usr/bin/env perl +# SPDX-License-Identifier: GPL-2.0-or-later + +# Copyright (C) 2019 Red Hat, Inc. + +# $ perldoc btmodem.pl if you'd like to read the manual, poor you: + +=head1 NAME + +btmodem.pl - emulate a bluetooth DUN modem + +=head1 SYNOPSIS + +btmodem.pl [<hci>] [-- <pppd> ...] + +=head1 DESCRIPTION + +B<btmodem.pl> registers a Bluetooth DUN profile with Bluez, accepts incoming +connections and pretends there's modem there. + +It answers a basic subset of AT commands, sufficient making ModemManager +recognize it as a 3GPP capable modem registered to a network. + +Upon receiving the dial (ATD) command, it spawns C<pppd> so that +NetworkManager can establish a connection. + +=head1 OPTIONS + +=over 4 + +=item B<< <hci> >> + +Create a service on this particular HCI. + +Defaults to I<hci0>. + +=item B<< <pppd> >> + +Specifies extra arguments to be prepended before C<pppd> to the default +set of I<nodetach notty local logfd 2 nopersist>. + +Defaults to I<pppd noauth dump debug 172.31.82.1:172.31.82.2>. + +=back + +=cut + +use strict; +use warnings; + +use IO::Handle; +use Net::DBus; +use Net::DBus::Reactor; + +# Parse command line arguments +my $hci_name; +my @pppd = qw/pppd noauth dump debug 172.31.82.1:172.31.82.2/; +while (@ARGV) { + $_ = shift @ARGV; + if ($_ eq '--') { + @pppd = @ARGV; + last; + } else { + die "Extra argument: '$_'" if $hci_name; + $hci_name = $_; + } +}; +$hci_name ||= 'hci0'; + +sub modemu +{ + my $fh = shift; + + while (<$fh>) { + chomp; + + if (/^AT$/ or /^ATE0$/ or /^ATV1$/ or /^AT\+CMEE=1$/ or /^ATX4$/ or /^AT&C1$/ or /^ATZ$/) { + # Standard Hayes commands that are basically used to + # ensure the modem is in a known state. Accept them all. + print $fh "\r\n"; + print $fh "OK\r\n"; + + } elsif (/^AT\+CPIN\?$/) { + # PIN unlocked. Required. + print $fh "\r\n"; + print $fh "+CPIN:READY\r\n"; + print $fh "\r\n"; + print $fh "OK\r\n"; + + } elsif (/^AT\+COPS=0$/) { + # Select access technology (we just accept 0=automatic) + print $fh "\r\n"; + print $fh "OK\r\n"; + + } elsif (/^AT\+CGREG\?$/) { + # 3GPP Registration status. + print $fh "\r\n"; + print $fh "+CGREG: 0,1\r\n"; + print $fh "\r\n"; + print $fh "OK\r\n"; + + } elsif (/^AT\+CGDCONT=\?$/) { + # Get supported PDP contexts + print $fh "\r\n"; + print $fh "+CGDCONT: (1-10),(\"IP\"),,,(0-1),(0-1)\r\n"; + print $fh "+CGDCONT: (1-10),(\"IPV6\"),,,(0-1),(0-1)\r\n"; + print $fh "OK\r\n"; + + } elsif (/^AT\+CGACT=0,1$/) { + # Activate a PDP context + print $fh "\r\n"; + print $fh "OK\r\n"; + + } elsif (/^AT\+CGDCONT=1,"(.*)","(.*)"$/) { + # Set PDP context. We accept any. + print $fh "\r\n"; + print $fh "OK\r\n"; + + } elsif (/^ATD/) { + print $fh "\r\n"; + print $fh "CONNECT 28800000\r\n"; + + my $ppp = fork; + die "Can't fork: $!" unless defined $ppp; + if ($ppp == 0) { + close STDIN; + close STDOUT; + open STDIN, '<&', $fh or die "Can't dup pty to a pppd stdin: $!"; + open STDOUT, '>&', $fh or die "Can't dup pty to a pppd stdout: $!"; + close $fh; + exec @pppd, qw/nodetach notty local logfd 2 nopersist/; + die "Can't exec pppd: $!"; + } + waitpid $ppp, 0; + } else { + print $fh "\r\n"; + print $fh "ERROR\r\n"; + } + } +} + +my $bus = Net::DBus->system; + +$bus->get_connection->register_object_path("/", sub { + my $bus = shift; + my $call = shift; + + # We only support the NewConnection call + next unless $call->get_type eq &Net::DBus::Binding::Message::MESSAGE_TYPE_METHOD_CALL; + if ( $call->get_interface ne 'org.bluez.Profile1' + or $call->get_path ne '/' + or $call->get_member ne 'NewConnection' + or $call->get_signature ne 'oha{sv}') { + + $bus->send ($bus->make_error_message ( + replyto => $call, + name => ' org.freedesktop.DBus.Error.Failed', + description => "Forgive me caller for I don't know what to do")); + next; + } + + my ($path, $fd, $args) = $call->get_args_list; + open (my $fh, "+>&=", $fd) or die $!; + + my $pid = fork; + die unless defined $pid; + + if ($pid == 0) { + # This allows us to use buffered read for lines from ModemManager + # despite not ending with \n + IO::Handle->input_record_separator ("\r"); + $fh->autoflush (1); + $fh->blocking (1); + modemu ($fh); + exit 0; + die; + } + + $bus->send ($bus->make_method_return_message ($call)) + unless $call->get_no_reply; +}); + +my $bluez = $bus->get_service ('org.bluez'); +my $profile_manager = $bluez->get_object ('/org/bluez', 'org.bluez.ProfileManager1'); + +$profile_manager->RegisterProfile('/', '00001103-0000-1000-8000-00805f9b34fb', {}); + +Net::DBus::Reactor->main->run; + +=head1 SETTING UP BLUETOOTH + +In order for this script useful, you need to have two Bluetooth interfaces +paired together. It's somewhat easier if you've got two machines to test. + +The pairing can be done withing the C<bluetoothctl> shell. Launch it after +you started C<btmodem.pl>, so that the right profile UUIDs are discovered +by the client. These commands come in handy: + +=over + +=item [bluetooth]# B<default-agent> + +This makes C<bluetoothctl> ask for pairing PIN in the shell session. That is +useful if you're ssh-ing into a machine instead of using a desktop shell with +its own agent. Run this on both machines. + +=item [bluetooth]# B<discoverable on> + +Broadcast the server service. You don't need to run this on the client. + +=item [bluetooth]# B<scan on> + +Turn on discovery of the devices. You need to don't run this on the server. + +After you've turned the discovery on, wait for a minute or so for your +server to get discovered. + +=item [bluetooth]# B<devices> + +List the known devices, both those who've been discovered and those that have +been paired with. + +=item [bluetooth]# B<pair 00:AA:01:00:00:23> + +Initiate the pairing. Run it from the machine that has scanning enabled. +Assumes your server is C<00:AA:01:00:00:23> -- check your real address with the +C<devices> command. + +After a short while, you should see the pairing confirmation prompt on both machines. + +=item [bluetooth]# B<trust 00:AA:01:00:00:24> + +Allow incoming connections from C<00:AA:01:00:00:24>. Run this on the server. + +=item B<nmcli c add type bluetooth ifname '*' gsm.apn internet bluetooth.type dun bluetooth.bdaddr 00:AA:01:00:00:23> + +If everything went right, you can now connect. + +=back + +=head1 EXAMPLES + +=over + +=item B<btmodem.pl> + +Just emulate a DUN modem on I<hci0>, with the default PPP arguments. + +=item B<btmodem.pl hci666> + +Same as above, just on the I<hci666> interface. + +=item B<btmodem.pl -- unshare --net pppd 172.31.82.1:172.31.82.2> + +Avoid polluting the namespace with the modem end of PPP connection. + +=item B<btmodem.pl -- pppd 10.0.0.1:10.0.0.2> + +Override the C<pppd> parameters: no debug logging and different set of +addresses. + +=item B<btmodem.pl mymodem -- pppd 10.0.0.1:10.0.0.2> + +Same as above, with a modem name different from default. + +=back + +=head1 BUGS + +Haha. You tell me. + +=head1 SEE ALSO + +L<ModemManager(8)>, L<pppd(8)>, C<modemu.pl> + +=head1 COPYRIGHT + +Copyright (C) 2019 Lubomir Rintel + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +=head1 AUTHOR + +Lubomir Rintel C<lkundrak@v3.sk> + +Like, it's me who wrote it, but if you're running it it's your problem. + +=cut diff --git a/contrib/scripts/checkpatch-feature-branch.sh b/contrib/scripts/checkpatch-feature-branch.sh new file mode 100755 index 00000000..d6f72b20 --- /dev/null +++ b/contrib/scripts/checkpatch-feature-branch.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +die() { + printf "%s\n" "$@" + exit 1 +} + +HEAD="${1:-HEAD}" + +BASE_DIR="$(dirname "$0")" + +if printf '%s' "$HEAD" | grep -q '\.\.'; then + # Check the explicitly specified range from the argument. + REFS=( $(git log --reverse --format='%H' "$HEAD") ) || die "not a valid range (HEAD is $HEAD)" +else + BASE_REF="refs/remotes/origin" + NM_UPSTREAM_REMOTE= + + if [ "$NM_CHECKPATCH_FETCH_UPSTREAM" == 1 ]; then + NM_UPSTREAM_REMOTE="nm-upstream-$(date '+%Y%m%d-%H%M%S')-$RANDOM" + git remote add "$NM_UPSTREAM_REMOTE" https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git + BASE_REF="refs/remotes/$NM_UPSTREAM_REMOTE" + git fetch origin "$(git rev-parse "$HEAD")" --no-tags --unshallow + git fetch "$NM_UPSTREAM_REMOTE" \ + --no-tags \ + "refs/heads/main:$BASE_REF/main" \ + "refs/heads/nm-*:$BASE_REF/nm-*" \ + || die "failure to fetch from https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git" + fi + + # the argument is only a single ref (or the default "HEAD"). + # Find all commits that branch off one of the stable branches or main + # and lead to $HEAD. These are the commits of the feature branch. + + RANGES=( $(git show-ref | sed 's#^\(.*\) '"$BASE_REF/"'\(main\|nm-1-[0-9]\+\)$#\1..'"$HEAD"'#p' -n) ) + + [ "${#RANGES[@]}" != 0 ] || die "cannot detect git-ranges (HEAD is $(git rev-parse "$HEAD"))" + + REFS=( $(git log --reverse --format='%H' "${RANGES[@]}") ) + + if [ "${#REFS[@]}" == 0 ] ; then + # no refs detected. This means, $HEAD is already on main (or one of the + # stable nm-1-* branches. Just check the patch itself. + REFS=( "$HEAD" ) + fi + + if [ -n "$NM_UPSTREAM_REMOTE" ]; then + git remote remove "$NM_UPSTREAM_REMOTE" + fi +fi + +SUCCESS=0 +for H in "${REFS[@]}"; do + export NM_CHECKPATCH_HEADER=$'\n'">>> VALIDATE \"$(git log --oneline -n1 "$H")\"" + git format-patch -U65535 --stdout -1 "$H" | "$BASE_DIR/checkpatch.pl" + if [ $? != 0 ]; then + SUCCESS=1 + fi +done + +exit $SUCCESS diff --git a/contrib/scripts/checkpatch-git-post-commit-hook b/contrib/scripts/checkpatch-git-post-commit-hook new file mode 100755 index 00000000..96479107 --- /dev/null +++ b/contrib/scripts/checkpatch-git-post-commit-hook @@ -0,0 +1,23 @@ +#!/bin/sh + +# contrib/scripts/checkpatch-git-post-commit-hook: +# Call this script via ".git/hooks/post-commit" + +DISABLED=${NM_HOOK_DISABLED:0} + +if [ "$DISABLED" == 1 ]; then + echo "COMMIT HOOK DISABLED" + exit 0 +fi + +FILE=contrib/scripts/checkpatch-feature-branch.sh +if [ -x "$FILE" ]; then + "$FILE" + exit 0 +fi + +FILE=contrib/scripts/checkpatch.pl +if [ -x "$FILE" ]; then + git format-patch -U65535 --stdout -1 | "$FILE" + exit 0 +fi diff --git a/contrib/scripts/checkpatch.pl b/contrib/scripts/checkpatch.pl new file mode 100755 index 00000000..c24db35e --- /dev/null +++ b/contrib/scripts/checkpatch.pl @@ -0,0 +1,319 @@ +#!/usr/bin/perl -n +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2018,2021 Red Hat, Inc. +# + +# $ perldoc checkpatch.pl for eye-pleasing view of the manual: + +=head1 NAME + +checkpatch.pl - check for common mistakes + +=head1 SYNOPSIS + +checkpatch.pl [<file> ...] + +=head1 DESCRIPTION + +B<checkpatch.pl> checks source files or patches for common mistakes. + +=head1 OPTIONS + +=over 4 + +=item B<< <file> >> + +A C source file or an unified diff. + +=back + +=cut + +use strict; +use warnings; + +chomp; + +our $is_patch; +our $is_file; +our $is_commit_message; + +our $seen_error; +our $line; # Current line +our $check_line; # Complain if errors are found on this line + +our @functions_seen; +our $type; +our $filename; +our $line_no; +our $indent; +our $check_is_todo; +our $expect_spdx; +our $subdir; + +sub new_hunk +{ + $type = undef; + $indent = undef; +} + +sub new_file +{ + $expect_spdx = 0; + $check_is_todo = 1; + $filename = $subdir // ''; + $filename .= shift; + @functions_seen = (); +} + +my $header = $ENV{'NM_CHECKPATCH_HEADER'}; + +sub complain +{ + my $message = shift; + my $plain_message = shift; + + return unless $check_line; + + if (defined($header)) { + warn "$header\n"; + undef $header; + } + + if ($plain_message) { + warn "$message\n"; + } else { + warn "$filename:$line_no: $message:\n"; + warn "> $line\n\n"; + } + $seen_error = 1; +} + +sub check_commit +{ + my $commit = shift; + my $required = shift; + my $commit_id; + my $commit_message; + + if ($commit =~ /^([0-9a-f]{5,})\b/) { + $commit_id = $1; + } else { + return unless $required; + } + + if ($commit_id and not system 'git rev-parse --git-dir >/dev/null 2>/dev/null') { + $commit_message = `git log --abbrev=12 --pretty=format:"%h ('%s')" -1 "$commit_id" 2>/dev/null`; + complain "Commit '$commit_id' does not seem to exist" unless $commit_message; + } + + $commit_message //= "<12 hex digits> ('<commit subject>')"; + complain "Refer to the commit id properly: $commit_message" unless $commit =~ /^[0-9a-f]{12} \('/; +} + +if ($is_patch) { + # This is a line of an unified diff + if (/^@@.*\+(\d+)/) { + $line_no = $1 - 1; + new_hunk; + next; + } + if (/^\+\+\+ (b\/)?(.*)/) { + new_file ($2); + next; + } + s/^([ \+])(.*)/$2/ or next; + $line_no++; + $check_line = $1 eq '+'; + $line = $2; +} elsif ($is_file) { + $line_no = $.; + $. = 0 if eof; + # This is a line from full C file + $check_line = 1; + $line = $_; +} elsif ($is_commit_message) { + $line_no++; + $filename = '(commit message)'; + $check_line = 1; + $line = $_; + /^---$/ and $is_commit_message = 0; + /^(Reverts|Fixes): *(.*)/ and check_commit ($2, 1); + /This reverts commit/ and next; + /cherry picked from/ and next; + /^git-subtree-dir: (.*)/ and $subdir = "$1/"; + /\bcommit (.*)/ and check_commit ($1, 0); + next; +} else { + # We don't handle these yet + /^diff --cc/ and exit 0; + $filename = ''; + $line_no = 1; + # We don't know if we're dealing with a patch or a C file yet + $is_commit_message = 1 if /^From \S/; + $is_file = 1 if /^#/; + $is_patch = 1 if /^---/; + next; +} + +if ($is_file and $filename ne $ARGV) { + new_file ($ARGV); + new_hunk; +} + +if ($filename !~ /\.[ch]$/) { + if ($check_is_todo) { + complain("Resolve todo list \"$filename\" first\n", 1) if $filename =~ /^TODO.txt$/; + $check_is_todo = 0; + } + next; +} + +next if $filename =~ /\/nm-[^\/]+-enum-types\.[ch]$/; +next if $filename =~ /\b(shared|src)\/systemd\// + and not $filename =~ /\/sd-adapt\// + and not $filename =~ /\/nm-/; +next if $filename =~ /\/(n-acd|c-list|c-siphash|n-dhcp4)\//; + +$expect_spdx = 1 if $line_no == 1; +$expect_spdx = 0 if $line =~ /SPDX-License-Identifier/; +complain ('Missing a SPDX-License-Identifier') if $line_no == 2 and $expect_spdx; + +complain ('Tabs are only allowed at the beginning of a line') if $line =~ /[^\t]\t/; +complain ('Trailing whitespace') if $line =~ /[ \t]$/; +complain ('Don\'t use glib typedefs for char/short/int/long/float/double') if $line =~ /\bg(char|short|int|long|float|double)\b/; +complain ("Don't use \"$1 $2\" instead of \"$2 $1\"") if $line =~ /\b(char|short|int|long) +(unsigned|signed)\b/; +complain ("Don't use \"unsigned int\" but just use \"unsigned\"") if $line =~ /\b(unsigned) +(int)\b/; +complain ("Please use LGPL-2.1-or-later SPDX tag for new files") if $is_patch and $line =~ /SPDX-License-Identifier/ and not /LGPL-2.1-or-later/; +complain ("Use a SPDX-License-Identifier instead of Licensing boilerplate") if $is_patch and $line =~ /under the terms of/; +complain ("Don't use space inside elvis operator ?:") if $line =~ /\?[\t ]+:/; +complain ("Don't add Emacs editor formatting hints to source files") if $line_no == 1 and $line =~ /-\*-.+-\*-/; +complain ("XXX marker are reserved for development while work-in-progress. Use TODO or FIXME comment instead?") if $line =~ /\bXXX\b/; +complain ("This gtk-doc annotation looks wrong") if $line =~ /\*.*\( *(transfer-(none|container|full)|allow none) *\) *(:|\()/; +complain ("The gtk-doc annotation (allow-none) is deprecated. Use either (nullable) and/or (optional). See https://gi.readthedocs.io/en/latest/annotations/giannotations.html#deprecated-gobject-introspection-annotations") if $line =~ /\*.*\( *(allow-none) *\) *(:|\()/; +complain ("Prefer nm_assert() or g_return*() to g_assert*()") if $line =~ /g_assert/ and (not $filename =~ /\/tests\//) and (not $filename =~ /\/nm-test-/); +complain ("Use gs_free_error with GError variables") if $line =~ /\bgs_free\b +GError *\*/; +complain ("Initialize GError variables to NULL, if you pass them on") if $line =~ /\bGError +\*([a-z0-9_]+);/; +complain ("Don't use strcmp/g_strcmp0 unless you need to sort. Consider nm_streq()/nm_streq0(),NM_IN_STRSET() for testing equality") if $line =~ /\b(strcmp|g_strcmp0)\b/; +complain ("Don't use API that uses the numeric source id. Instead, use GSource and API like nm_g_idle_add(), nm_g_idle_add_source(), nm_clear_g_source_inst(), etc.") if $line =~ /\b(g_idle_add|g_idle_add_full|g_timeout_add|g_timeout_add_seconds|g_source_remove|nm_clear_g_source)\b/; +complain ("Prefer g_snprintf() over snprintf() (for consistency)") if $line =~ /\b(snprintf)\b/; +complain ("Prefer nm_str_hash()/nm_direct_hash() over g_str_hash()/g_direct_hash(). Those use siphash24") if $line =~ /\b(g_str_hash|g_direct_hash)\b/; +complain ("Don't use g_direct_equal() for hash tables, pass NULL for pointer equality which avoids the function call") if $line =~ /\b(g_direct_equal)\b/; +complain ("Prefer nm_pint_hash()/nm_pint64_hash()/nm_pdouble_hash() over g_int_hash()/g_int64_hash()/g_double_hash(). Those use siphash24") if $line =~ /\b(g_int_hash|g_int64_hash|g_double_hash)\b/; +complain ("Prefer nm_pint_equal()/nm_pint64_equal()/nm_pdouble_equal() over g_int_equal()/g_int64_equal()/g_double_equal(). Those names mirror our nm_p*_hash() functions") if $line =~ /\b(g_int_equal|g_int64_equal|g_double_equal)\b/; +complain ("Avoid g_clear_pointer() and use nm_clear_pointer() (or nm_clear_g_free(), g_clear_object(), etc.)") if $line =~ /\b(g_clear_pointer)\b/; +complain ("Define setting properties with _nm_setting_property_define_direct_*() API") if $line =~ /g_param_spec_/ and $filename =~ /\/libnm-core-impl\/nm-setting/; +complain ("Use nm_g_array_{index,first,last,index_p}() instead of g_array_index(), as it nm_assert()s for valid element size and out-of-bound access") if $line =~ /\bg_array_index\b/; +complain ("Use spaces instead of tabs") if $line =~ /\t/; +complain ("Prefer implementing private pointers via _NM_GET_PRIVATE() or _NM_GET_PRIVATE_PTR() (the latter, if the private data has an opqaue pointer in the header file)") if $line =~ /\b(g_type_class_add_private|G_TYPE_INSTANCE_GET_PRIVATE)\b/; +complain ("Don't use close()/g_close(). Instead, use nm_close() (or nm_close_with_error()).") if $line =~ /\b(close|g_close)\b *\(/; +complain ("Use nm_memdup() instead of g_memdup(). The latter has a size argument of type guint") if $line =~ /\bg_memdup\b/; + +# Further on we process stuff without comments. +$_ = $line; +s/\s*\/\*.*\*\///; +s/\s*\/\*.*//; +s/\s*\/\/.*//; +/^\s* \* / and next; + +if (/^typedef*/) { + # We expect the { on the same line as the typedef. Otherwise it + # looks too much like a function declaration + complain ('Unexpected line break following a typedef') unless /[;{,]$/; + next; +} elsif (/^[A-Za-z_][A-Za-z0-9_ ]*\*?$/ and /[a-z]/) { + # A function type + $type = $_; + next; +} elsif ($type and /^([A-Za-z_][A-Za-z0-9_]*)(\s*)\(/) { + my @order = qw/^get_property$ ^set_property$ (?<!_iface|_class)_init$ ^constructor$ + ^constructed$ _new$ ^dispose$ ^finalize$ _class_init$/; + my @following = (); + my @tmp = (); + + # A function name + my $name = $1; + complain ('No space between function name and arguments') unless $2 eq ''; + + # Determine which function must not be preceding this one + foreach my $func (reverse @order) { + if ($name =~ /$func/) { + @following = @tmp; + last; + } + push @tmp, $func; + } + + # Check if an out-of-order function was seen + foreach my $func (@following) { + my @wrong = grep { /$func/ } @functions_seen; + complain (join (', ', map { "'$_'" } @wrong)." should follow '$name'") if @wrong; + } + + push @functions_seen, $1; + $type = undef; + next; +} + +if ($type) { + # We've seen what looked like a type in a function declaration, + # but the function declaration didn't follow. + if ($type =~ /^(struct|union)/ and $line eq '{') { + complain ("Brace should be one the same line as the '$type' declaration"); + } else { + complain ("Expected a function declaration following '$type', but found something else"); + } + $type = undef; +} + +END { + if ($seen_error) { + warn "The patch does not validate.\n" if $is_patch; + warn "The file does not validate.\n" if $is_file; + $? = 1 + } +}; + +=head1 EXAMPLES + +=over + +=item B<checkpatch.pl hello.c> + +Check a single file. + +=item B<git diff --cached |checkpatch.pl> + +Check the currently staged changes. + +=item B<git format-patch -U65535 --stdout -1 |contrib/scripts/checkpatch.pl || :> + +A F<.git/hooks/post-commit> oneliner that, wisely, tolerates failures while +still providing advice. The large line context allows helps checkpatch.pl +get a better idea about the changes in context of code that does not change. + +=back + +=head1 BUGS + +Proabably too many. + +=head1 SEE ALSO + +F<CONTRIBUTING> + +=head1 COPYRIGHT + +Copyright (C) 2018,2021 Red Hat + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +=head1 AUTHOR + +Lubomir Rintel C<lkundrak@v3.sk> + +=cut diff --git a/contrib/scripts/code-style-git-post-commit-hook b/contrib/scripts/code-style-git-post-commit-hook new file mode 100755 index 00000000..8d464842 --- /dev/null +++ b/contrib/scripts/code-style-git-post-commit-hook @@ -0,0 +1,21 @@ +#!/bin/sh + +set -e + +DISABLED=${NM_HOOK_DISABLED:0} + +if [ "$DISABLED" == 1 ]; then + echo "COMMIT HOOK DISABLED" + exit 0 +fi + +FORMATTER=contrib/scripts/nm-code-format.sh + +# Filter only C source files +CHANGED_FILES=$(git log --pretty='' --name-only -n1 | grep -E '\.c$|\.h$' | tr '\n' ' ') + +echo $CHANGED_FILES + +if [ -x "$FORMATTER" ] && [ ! -z "$CHANGED_FILES" ]; then + "$FORMATTER" -n "${CHANGED_FILES}" +fi diff --git a/contrib/scripts/find-backports b/contrib/scripts/find-backports new file mode 100755 index 00000000..c2082570 --- /dev/null +++ b/contrib/scripts/find-backports @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 + +import subprocess +import collections +import os +import sys +import re +import pprint + + +FNULL = open(os.devnull, "w") +pp = pprint.PrettyPrinter(indent=4, stream=sys.stderr) + +DEBUG = os.environ.get("NM_FIND_BACKPORTS_DEBUG", None) == "1" + + +def dbg_log(s): + if DEBUG: + print(s, file=sys.stderr) + + +def dbg_pprint(obj): + if DEBUG: + pp.pprint(obj) + + +def print_err(s): + print(s, file=sys.stderr) + + +def die(s): + print_err(s) + sys.exit(1) + + +def memoize(f): + memo = {} + + def helper(x): + if x not in memo: + memo[x] = f(x) + return memo[x] + + return helper + + +def re_bin(r): + return r.encode("utf8") + + +def _keys_to_dict(itr): + d = collections.OrderedDict() + for c in itr: + d[c] = None + return d + + +@memoize +def git_ref_exists_full_path(ref): + val = git_ref_exists(ref) + if val: + try: + subprocess.check_output(["git", "show-ref", "-q", "--verify", str(ref)]) + except subprocess.CalledProcessError: + pass + else: + return val + return None + + +def _git_ref_exists_eval(ref): + try: + out = subprocess.check_output( + ["git", "rev-parse", "--verify", str(ref) + "^{commit}"], + stderr=FNULL, + ) + except subprocess.CalledProcessError: + return None + o = out.decode("ascii").strip() + if len(o) == 40: + return o + raise Exception(f"git-rev-parse for '{ref}' returned unexpected output {out}") + + +_git_ref_exists_cache = {} + + +def git_ref_exists(ref): + val = _git_ref_exists_cache.get(ref, False) + + if val is False: + val = _git_ref_exists_eval(ref) + _git_ref_exists_cache[ref] = val + if val and ref != val: + _git_ref_exists_cache[val] = val + + return val + + +@memoize +def git_get_head_name(ref): + out = subprocess.check_output( + ["git", "rev-parse", "--symbolic-full-name", str(ref)], stderr=FNULL + ) + return out.decode("utf-8").strip() + + +def git_merge_base(a, b): + out = subprocess.check_output(["git", "merge-base", str(a), str(b)], stderr=FNULL) + out = out.decode("ascii").strip() + assert git_ref_exists(out) + return out + + +def git_all_commits_grep(rnge, grep=None): + if grep: + grep = [("--grep=%s" % g) for g in grep] + notes = ["-c", "notes.displayref=refs/notes/bugs"] + else: + grep = [] + notes = [] + out = subprocess.check_output( + ["git"] + + notes + + ["log", "--pretty=%H", "--notes", "--reverse"] + + grep + + [str(rnge)], + stderr=FNULL, + ) + return [x for x in out.decode("ascii").split("\n") if x] + + +def git_logg(commits): + commits = list(commits) + if not commits: + return "" + out = subprocess.check_output( + [ + "git", + "log", + "--no-show-signature", + "--no-walk", + "--pretty=format:%Cred%h%Creset - %Cgreen(%ci)%Creset [%C(yellow)%an%Creset] %s%C(yellow)%d%Creset", + "--abbrev-commit", + "--date=local", + ] + + [str(c) for c in commits], + stderr=FNULL, + ) + return out.decode("utf-8").strip() + + +@memoize +def git_all_commits(rnge): + return git_all_commits_grep(rnge) + + +@memoize +def git_all_commits_set(rnge): + return set(git_all_commits_grep(rnge)) + + +def git_commit_sorted(commits): + commits = list(commits) + if not commits: + return [] + out = subprocess.check_output( + ["git", "log", "--no-walk", "--pretty=%H", "--reverse"] + + [str(x) for x in commits], + stderr=FNULL, + ) + out = out.decode("ascii") + return [x for x in out.split("\n") if x] + + +@memoize +def git_ref_commit_body(ref): + return subprocess.check_output( + [ + "git", + "-c", + "notes.displayref=refs/notes/bugs", + "log", + "-n1", + "--pretty=%B%n%N", + str(ref), + ], + stderr=FNULL, + ) + + +@memoize +def git_ref_commit_body_get_fixes(ref): + body = git_ref_commit_body(ref) + result = [] + for mo in re.finditer(re_bin("\\b[fF]ixes: *([0-9a-z]+)\\b"), body): + c = mo.group(1).decode("ascii") + h = git_ref_exists(c) + if h: + result.append(h) + if result: + # The commit that contains a "Fixes:" line, can also contain an "Ignore-Fixes:" line + # to disable it. This only makes sense with refs/notes/bugs notes, to fix up a wrong + # annotation. + for mo in re.finditer(re_bin("\\bIgnore-[fF]ixes: *([0-9a-z]+)\\b"), body): + c = mo.group(1).decode("ascii") + h = git_ref_exists(c) + try: + result.remove(h) + except ValueError: + pass + + return result + + +@memoize +def git_ref_commit_body_get_cherry_picked_one(ref): + ref = git_ref_exists(ref) + if not ref: + return None + body = git_ref_commit_body(ref) + result = None + for r in [ + re_bin("\\(cherry picked from commit ([0-9a-z]+)\\)"), + re_bin("\\bIgnore-Backport: *([0-9a-z]+)\\b"), + ]: + for mo in re.finditer(r, body): + c = mo.group(1).decode("ascii") + h = git_ref_exists(c) + if h: + if not result: + result = [h] + else: + result.append(h) + return result + + +@memoize +def git_ref_commit_body_get_cherry_picked_recurse(ref): + ref = git_ref_exists(ref) + if not ref: + return None + + def do_recurse(result, ref): + result2 = git_ref_commit_body_get_cherry_picked_one(ref) + if result2: + extra = [h2 for h2 in result2 if h2 not in result] + if extra: + result.extend(extra) + for h2 in extra: + do_recurse(result, h2) + + result = [] + do_recurse(result, ref) + return result + + +def git_commits_annotate_fixes(rnge): + commits = git_all_commits(rnge) + c_dict = _keys_to_dict(commits) + for c in git_all_commits_grep(rnge, grep=["[Ff]ixes:"]): + ff = git_ref_commit_body_get_fixes(c) + if ff: + c_dict[c] = ff + return c_dict + + +def git_commits_annotate_cherry_picked(rnge): + commits = git_all_commits(rnge) + c_dict = _keys_to_dict(commits) + for c in git_all_commits_grep( + ref_head, grep=["cherry picked from commit", "Ignore-Backport:"] + ): + ff = git_ref_commit_body_get_cherry_picked_recurse(c) + if ff: + c_dict[c] = ff + return c_dict + + +def git_ref_in_history(ref, rnge): + return git_ref_exists(ref) in git_all_commits_set(rnge) + + +if __name__ == "__main__": + if len(sys.argv) <= 1: + ref_head0 = "HEAD" + else: + ref_head0 = sys.argv[1] + + ref_head = git_ref_exists(ref_head0) + if not ref_head: + die('Ref "%s" does not exist' % (ref_head0)) + + if not git_ref_exists_full_path("refs/notes/bugs"): + die( + "Notes refs/notes/bugs not found. Read CONTRIBUTING.md file for how to setup the notes" + ) + + ref_upstreams = [] + if len(sys.argv) <= 2: + head_name = git_get_head_name(ref_head0) + match = False + if head_name: + match = re.match("^refs/(heads|remotes/[^/]*)/nm-1-([0-9]+)$", head_name) + if match: + i = int(match.group(2)) + while True: + i += 2 + r = "nm-1-" + str(i) + if not git_ref_exists(r): + r = "refs/remotes/origin/nm-1-" + str(i) + if not git_ref_exists(r): + break + ref_upstreams.append(r) + ref_upstreams.append("main") + + if not ref_upstreams: + if len(sys.argv) <= 2: + ref_upstreams = ["main"] + else: + ref_upstreams = list(sys.argv[2:]) + + for h in ref_upstreams: + if not git_ref_exists(h): + die('Upstream ref "%s" does not exist' % (h)) + + print_err("Check %s (%s)" % (ref_head0, ref_head)) + print_err("Upstream refs: %s" % (ref_upstreams)) + + print_err('Check patches of "%s"...' % (ref_head)) + own_commits_list = git_all_commits(ref_head) + own_commits_cherry_picked = git_commits_annotate_cherry_picked(ref_head) + + cherry_picks_all = collections.OrderedDict() + for c, cherry_picked in own_commits_cherry_picked.items(): + if cherry_picked: + for c2 in cherry_picked: + l = cherry_picks_all.get(c2) + if not l: + cherry_picks_all[c2] = [c] + else: + l.append(c) + + own_commits_cherry_picked_flat = set() + for c, p in own_commits_cherry_picked.items(): + own_commits_cherry_picked_flat.add(c) + if p: + own_commits_cherry_picked_flat.update(p) + + dbg_log(">>> own_commits_cherry_picked") + dbg_pprint(own_commits_cherry_picked) + + dbg_log(">>> cherry_picks_all") + dbg_pprint(cherry_picks_all) + + # find all commits on the upstream branches that fix another commit. + fixing_commits = {} + for ref_upstream in ref_upstreams: + ref_str = ref_head + ".." + ref_upstream + print_err(f'Check upstream patches "{ref_str}"...') + for c, fixes in git_commits_annotate_fixes(ref_str).items(): + if not fixes: + dbg_log(f">>> test {c} : SKIP (does not fix anything)") + continue + if c in cherry_picks_all: + # commit 'c' is already backported. Skip it. + dbg_log(f">>> test {c} => {fixes} : SKIP (already backported)") + continue + dbg_log(f">>> test {c} => {fixes} : process") + for f in fixes: + if f not in own_commits_cherry_picked_flat: + # commit "c" fixes commit "f", but this is not one of our own commits + # and not interesting. + dbg_log(f">>> fixes {f} not in own_commits_cherry_picked") + continue + dbg_log(f">>> take {c} (fixes {fixes})") + fixing_commits[c] = fixes + break + + extra = collections.OrderedDict( + [(c, git_ref_commit_body_get_cherry_picked_recurse(c)) for c in fixing_commits] + ) + extra2 = [] + for c in extra: + is_back = False + for e_v in extra.values(): + if c in e_v: + is_back = True + break + if not is_back: + extra2.append(c) + + commits_good = extra2 + + commits_good = git_commit_sorted(commits_good) + + print_err(git_logg(commits_good)) + + not_in = [ + c + for c in commits_good + if not git_ref_in_history(c, f"{ref_head}..{ref_upstreams[0]}") + ] + if not_in: + print_err("") + print_err( + f'WARNING: The following commits are not from the first reference "{ref_upstreams[0]}".' + ) + print_err( + f' You may want to first backports those patches to "{ref_upstreams[0]}".' + ) + for l in git_logg(git_commit_sorted(not_in)).splitlines(): + print_err(f" - {l}") + print_err("") + + for c in reversed(commits_good): + print("%s" % (c)) diff --git a/contrib/scripts/git-backport-merge b/contrib/scripts/git-backport-merge new file mode 100755 index 00000000..8ba2244f --- /dev/null +++ b/contrib/scripts/git-backport-merge @@ -0,0 +1,58 @@ +#!/bin/bash + +# Uses `git cherry-pick -x` to backport a merge commit to an older branch. +# +# Usage: +# First checkout the old-stable branch, that is the target for the backport. +# Then `git-backport-merge MERGE_REF [REFS...]` +# MERGE_REF is the merge commit that should be backported. +# [REFS...] is the commits that should be backported. If omitted, +# it automatically takes the parent commits of the merge commit. + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +backport_merge() { + test "$#" -gt 0 || die "Requires the commit ref to backport (and optimally select the commits to include)" + + local M="${@:$#}" + local h + + if test "$#" -eq 1; then + local C=($(git log --reverse "--pretty=%H" "$M"^1.."$M"^2)) + else + local C=("${@:1:$#-1}") + fi + + local OLD_HEAD="$(git rev-parse HEAD)" || die "failed to get current HEAD" + + test -n "$(git status --porcelain --untracked-files=no)" && die "Working directory contains changes. Abort." + + local M_ID="$(git rev-parse "$M"^{commit})" || die "\"$M\" is not a valid commit" + + trap EXIT 'test -z "$OLD_HEAD" || git reset "$OLD_HEAD" --hard' + + for h in "${C[@]}"; do + if ! git cherry-pick --allow-empty -x "$h" ; then + git cherry-pick --abort + die "failed to cherry-pick commit \"$h\" on top of \"$(git rev-parse HEAD)\"" + fi + done + + local NEW_HEAD="$(git rev-parse HEAD)" || die "failed to get new HEAD" + + git reset --hard "$OLD_HEAD" || die "Failed to reset to previous HEAD \"$OLD_HEAD\"" + + git merge --no-ff --no-edit "$NEW_HEAD" || die "Failed to merge old HEAD \"$OLD_HEAD\" with new \"$NEW_HEAD\"" + + git commit --amend --allow-empty -C "$M" || die "Failed to amend merge commit \"$(git rev-parse HEAD)\" with commit message from \"$M\"" + + git rev-parse "$M" | sed 's/.*/(cherry picked from commit \0)/' | GIT_EDITOR='sh -c "cat >> \"$1\""' git commit --allow-empty --amend || \ + die "Failed to amend merge commit \"$(git rev-parse HEAD)\" with cherry-picked-from message from \"$M\"" + + OLD_HEAD= +} + +backport_merge "$@" diff --git a/contrib/scripts/git-subtree-reimport.sh b/contrib/scripts/git-subtree-reimport.sh new file mode 100755 index 00000000..73319044 --- /dev/null +++ b/contrib/scripts/git-subtree-reimport.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# In our git repository we vendor in several external projects. +# We do so via git-subtree. +# +# Run this script (without arguments) for re-importing the latest +# version of those projects. +# +# You can also specify the projects to reimport on the command line, +# ./contrib/scripts/git-subtree-reimport.sh [ c-list | c-rbtree | c-siphash | c-stdaux | n-acd | n-dhcp4 ... ] + +set -e + +cd "$(dirname "$(readlink -f "$0")")/../.." + +reimport() { + local d="$1" + local project + local branch + + if [[ "$d" = c-* ]] ; then + project=c-util + branch=main + else + project=nettools + branch=master + fi + + CMD=( git subtree pull --prefix "src/$d" "git@github.com:$project/$d.git" "$branch" --squash -m \ +"$d: re-import git-subtree for 'src/$d' + + git subtree pull --prefix src/$d git@github.com:$project/$d.git $branch --squash +" ) + + printf '\n>>>> %s >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n' "$d" + printf '>>>' + for c in "${CMD[@]}"; do + printf ' %q' "$c" + done + printf '\n' + + "${CMD[@]}" 2>&1 + + local REMOTE_COMMIT="$(git rev-parse FETCH_HEAD)" + + echo ">>>>> RESULT:" + printf ">>> git diff %s: HEAD:src/%s\n" "$REMOTE_COMMIT" "$d" + GIT_PAGER=cat git diff --color=always "$REMOTE_COMMIT:" "HEAD:src/$d" +} + +reimport_all() { + local ARGS + + ARGS=( "$@" ) + if [ "${#ARGS[@]}" = 0 ]; then + ARGS=( c-list c-rbtree c-siphash c-stdaux n-acd n-dhcp4 ) + fi + for d in "${ARGS[@]}" ; do + reimport "$d" + done +} + +reimport_all "$@" diff --git a/contrib/scripts/modemu.pl b/contrib/scripts/modemu.pl new file mode 100755 index 00000000..0e90fafa --- /dev/null +++ b/contrib/scripts/modemu.pl @@ -0,0 +1,299 @@ +#!/usr/bin/env perl +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2018 Red Hat, Inc. +# + +# $ perldoc modemu.pl for eye-pleasing view of the manual: + +=head1 NAME + +modemu.pl - emulate a serial modem + +=head1 SYNOPSIS + +modemu.pl [<name>] [-- <pppd> ...] + +=head1 DESCRIPTION + +B<modemu.pl> opens a PTY, links the slave side to F</dev> and announces a +fake kobject via netlink as if it were a real serial device, so that +ModemManager picks it up. + +Then it answers to a very basic subset of AT commands, sufficient making +ModemManager recognize it as a 3GPP capable modem registered to a network. + +Upon receiving the dial (ATD) command, it spawns C<pppd> so that +NetworkManager can establish a connection. + +B<modemu.pl> needs superuser privileges to be able to announce a kobject +and create a F</dev> node. + +=head1 OPTIONS + +=over 4 + +=item B<< <name> >> + +Create a modem of given name. Links it to F<< /dev/<name> >>. + +Defaults to I<modemu>. + +=item B<< <pppd> >> + +Specifies extra arguments to be prepended before C<pppd> to the default +set of I<nodetach notty local logfd 2 nopersist>. + +Defaults to I<pppd dump debug 172.31.82.1:172.31.82.2>. + +=back + +=cut + +use strict; +use warnings; + +use Errno; +use Socket; +use IO::Pty; +use IO::Handle; + +use constant AF_NETLINK => 16; +use constant NETLINK_KOBJECT_UEVENT => 15; + +# This allows us to use buffered read for lines from ModemManager +# despite not ending with \n +IO::Handle->input_record_separator ("\r"); + +# Parse command line arguments +my $name; +my @pppd = qw/pppd dump debug 172.31.82.1:172.31.82.2/; +while (@ARGV) { + $_ = shift @ARGV; + if ($_ eq '--') { + @pppd = @ARGV; + last; + } else { + die "Extra argument: '$_'" if $name; + $name = $_; + } +}; +$name ||= 'modemu'; + +socket my $fd, AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT + or die "Can't create a netlink socket: $!"; + +my $seqnum = 666; +sub send_netlink +{ + my %props = (@_, SEQNUM => $seqnum++); + my $props = join '', map { $_, '=', $props{$_}, "\0" } keys %props; + + my $head = pack 'a8NLLLNLLL', + # signature + magic + 'libudev', + 0xfeedcafe, + + # 40 octets is the length of this header + 40, 40, 40 + length ($props), + + # Digest::MurmurHash2::Neutral::murmur_hash2_neutral("tty") + 0x8afa90c8, + + 0x00000000, + 0x00040002, + 0x00008010; + + $! = undef; + send $fd, "$head$props", 0, pack 'SSLL', AF_NETLINK, 0, 0, 0x0002; + # RHEL 7 kernel responds ECONNREFUSED even thoguh the sendto succeeded. Weird. + die "Can't send a netlink message: $!" if $! and not $!{ECONNREFUSED}; +} + +my $devpath = '/devices/pci0000:00/0000:00:00.0'; +unless (-d "/sys/$devpath") { + # Create a virtual device. Older ModemManager likes a hotpluggable bus + # (USB, PCI), but there's none on an IBM POWER lpar... + warn "No PCI bus to use for parent. Don't expect this to work with ModemManager 1.6"; + $devpath = '/devices/virtual'; +} + +my %props = ( + DEVPATH => "$devpath/$name", + SUBSYSTEM => 'tty', + DEVNAME => "/dev/$name", + + # Whitelisting that works for both ModemManager 1.6 and 1.8 + ID_MM_CANDIDATE => '1', + ID_MM_DEVICE_PROCESS => '1', +); + +sub cleanup +{ + unlink "/dev/$name"; + send_netlink (ACTION => 'remove', %props) if $fd; +} + +# Ensure we clean up before and after. +END { cleanup }; +$SIG{INT} = sub { cleanup; die }; +$SIG{TERM} = sub { cleanup; die }; +cleanup; + +my $pty = new IO::Pty; +my $ptyname = ttyname $pty; +symlink $ptyname, "/dev/$name" or die "Can't create /dev/$name: $!"; +send_netlink (ACTION => 'add', %props); +my ($pdptype, $apn); + +# Here's a good refernce of AT command a modern-ish modem probably uses: +# https://infocenter.nordicsemi.com/index.jsp?topic=%2Fref_at_commands%2FREF%2Fat_commands%2Fpacket_domain%2Fcgact_set.html + +while (<$pty>) { + chomp; + + if (/^AT$/ or /^ATE0$/ or /^ATV1$/ or /^AT\+CMEE=1$/ or /^ATX4$/ or /^AT&C1$/ or /^ATZ$/) { + # Standard Hayes commands that are basically used to + # ensure the modem is in a known state. Accept them all. + print $pty "\r\n"; + print $pty "OK\r\n"; + + } elsif (/^AT\+CPIN\?$/) { + # PIN unlocked. Required. + print $pty "\r\n"; + print $pty "+CPIN:READY\r\n"; + print $pty "\r\n"; + print $pty "OK\r\n"; + + } elsif (/^AT\+COPS=0$/) { + # Select access technology (we just accept 0=automatic) + print $pty "\r\n"; + print $pty "OK\r\n"; + + } elsif (/^AT\+CGREG\?$/) { + # 3GPP Registration status. + print $pty "\r\n"; + print $pty "+CGREG: 0,1\r\n"; + print $pty "\r\n"; + print $pty "OK\r\n"; + + # The PDP (packet data protocol/profile?) context handling below is very + # rudimentary: just enough to keep ModemManager 1.18 happy. It basically + # just starts with no contexts at all and then expects MM to set and + # activate profile number 1. + + } elsif (/^AT\+CGDCONT=\?$/) { + # Get supported PDP contexts + print $pty "\r\n"; + print $pty "+CGDCONT: (1-10),(\"IP\"),,,(0-1),(0-1)\r\n"; + print $pty "+CGDCONT: (1-10),(\"IPV6\"),,,(0-1),(0-1)\r\n"; + print $pty "OK\r\n"; + + } elsif (/^AT\+CGDCONT=1,"(.*)","(.*)"$/) { + # Create the PDP context. Remember it, MM is going to check it later + ($pdptype, $apn) = ($1, $2); + print $pty "\r\n"; + print $pty "OK\r\n"; + + + } elsif (/^AT\+CGDCONT\?$/) { + # List the PDP context we're aware of. + print $pty "\r\n"; + print $pty "+CGDCONT: 1,\"$pdptype\",\"$apn\",\"0.0.0.0\",0,0,0,0\r\n" + if defined $pdptype; + print $pty "OK\r\n"; + + } elsif (/^AT\+CGACT\?$/) { + # List available PDP contexts with states: profile 1 state 0 (inactive) + print $pty "\r\n"; + print $pty "+CGACT: 0,1\r\n"; + print $pty "OK\r\n"; + + } elsif (/^AT\+CGACT=0,1$/) { + # Deactivate a PDP context + print $pty "\r\n"; + print $pty "OK\r\n"; + + } elsif (/^AT\+COPS\?$/) { + # Current operators + # Not strictly required, but allows NetworkManager to just connect + # the modem device without explicitly setting an APN + print $pty "\r\n"; + print $pty "+COPS: 0,2,\"65302\",7\r\n"; # MCCMNC + print $pty "OK\r\n"; + + } elsif (/^ATD/) { + print $pty "\r\n"; + print $pty "CONNECT 28800000\r\n"; + + my $ppp = fork; + die "Can't fork: $!" unless defined $ppp; + if ($ppp == 0) { + close STDIN; + close STDOUT; + open STDIN, '<&', $pty or die "Can't dup pty to a pppd stdin: $!"; + open STDOUT, '>&', $pty or die "Can't dup pty to a pppd stdout: $!"; + close $pty; + exec @pppd, qw/nodetach notty local logfd 2 nopersist/; + die "Can't exec pppd: $!"; + } + waitpid $ppp, 0; + } else { + print $pty "\r\n"; + print $pty "ERROR\r\n"; + } +} + +=head1 EXAMPLES + +=over + +=item B<modemu.pl> + +Just create a modem named I<modemu>, with the default PPP arguments. + +=item B<modemu.pl ttyS666> + +Same as above, just name the modem I<ttyS666>. + +=item B<modemu.pl -- unshare --net pppd 172.31.82.1:172.31.82.2> + +Avoid polluting the namespace with the modem end of PPP connection. + +=item B<modemu.pl -- pppd 10.0.0.1:10.0.0.2> + +Override the C<pppd> parameters: no debug logging and different set of +addresses. + +=item B<modemu.pl mymodem -- pppd 10.0.0.1:10.0.0.2> + +Same as above, with a modem name different from default. + +=back + +=head1 BUGS + +Only works on machines with a PCI bus. ModemManager is picky about platform +devices and accepts PCI and USB buses easily. Which is why pretent to have +our tty on the PCI root device. + +Terminates after a single PPP session. C<pppd> seems to hang up the PTY. + +=head1 SEE ALSO + +L<ModemManager(8)>, L<pppd(8)> + +=head1 COPYRIGHT + +Copyright (C) 2018 Lubomir Rintel + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +=head1 AUTHOR + +Lubomir Rintel C<lkundrak@v3.sk> + +=cut diff --git a/contrib/scripts/nm-ci-patch-gtkdoc.sh b/contrib/scripts/nm-ci-patch-gtkdoc.sh new file mode 100755 index 00000000..e72a0a8f --- /dev/null +++ b/contrib/scripts/nm-ci-patch-gtkdoc.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# patch gtk-doc for https://gitlab.gnome.org/GNOME/gtk-doc/merge_requests/2 + +cd / + +patch -f -p 1 --fuzz 0 --reject-file=- <<EOF +diff --git a/usr/share/gtk-doc/python/gtkdoc/scan.py b/usr/share/gtk-doc/python/gtkdoc/scan.py +index f1f167235ab2e4c62676fbcfb87ebbe55c95b944..b59dd17abfa5f42b7bb06d239f9c78e5efffbf5d 100644 +--- a/usr/share/gtk-doc/python/gtkdoc/scan.py ++++ b/usr/share/gtk-doc/python/gtkdoc/scan.py +@@ -427,20 +427,26 @@ def ScanHeader(input_file, section_list, decl_list, get_types, options): + elif m9: + # We've found a 'typedef struct _<name> <name>;' + # This could be an opaque data structure, so we output an + # empty declaration. If the structure is actually found that + # will override this. + structsym = m9.group(1).upper() + logging.info('%s typedef: "%s"', structsym, m9.group(2)) + forward_decls[m9.group(2)] = '<%s>\n<NAME>%s</NAME>\n%s</%s>\n' % ( + structsym, m9.group(2), deprecated, structsym) + ++ bm = re.search(r'^(\S+)(Class|Iface|Interface)\b', m9.group(2)) ++ if bm: ++ objectname = bm.group(1) ++ logging.info('Found object: "%s"', objectname) ++ title = '<TITLE>%s</TITLE>' % objectname ++ + elif re.search(r'^\s*(?:struct|union)\s+_(\w+)\s*;', line): + # Skip private structs/unions. + logging.info('private struct/union') + + elif m10: + # Do a similar thing for normal structs as for typedefs above. + # But we output the declaration as well in this case, so we + # can differentiate it from a typedef. + structsym = m10.group(1).upper() + logging.info('%s:%s', structsym, m10.group(2)) +EOF + diff --git a/contrib/scripts/nm-ci-run.sh b/contrib/scripts/nm-ci-run.sh new file mode 100755 index 00000000..e385ef1e --- /dev/null +++ b/contrib/scripts/nm-ci-run.sh @@ -0,0 +1,299 @@ +#!/bin/bash + +# Arguments via environment variables: +# - CI +# - CC +# - BUILD_TYPE +# - CFLAGS +# - WITH_DOCS + +set -ex + +die() { + printf "%s\n" "$@" + exit 1 +} + +_is_true() { + case "$1" in + 1|y|yes|YES|Yes|on) + return 0 + ;; + 0|n|no|NO|No|off) + return 1 + ;; + "") + if [ "$2" == "" ]; then + die "not a boolean argument \"$1\"" + fi + _is_true "$2" + return $? + ;; + *) + die "not a boolean argument \"$1\"" + ;; + esac +} + +USE_CCACHE=0 +if command -v ccache &>/dev/null; then + USE_CCACHE=1 + export PATH="/usr/lib64/ccache:/usr/lib/ccache${PATH:+:${PATH}}" +fi + +IS_FEDORA=0 +IS_CENTOS=0 +IS_ALPINE=0 +grep -q '^NAME=.*\(CentOS\)' /etc/os-release && IS_CENTOS=1 +grep -q '^NAME=.*\(Fedora\)' /etc/os-release && IS_FEDORA=1 +grep -q '^NAME=.*\(Alpine\)' /etc/os-release && IS_ALPINE=1 + +############################################################################### + +if [ "$BUILD_TYPE" == meson ]; then + _TRUE=true + _FALSE=false +elif [ "$BUILD_TYPE" == autotools ]; then + _TRUE=yes + _FALSE=no +else + die "invalid \$BUILD_TYPE \"$BUILD_TYPE\"" +fi + +_WITH_CRYPTO="gnutls" +_WITH_WERROR=1 +_WITH_LIBTEAM="$_TRUE" +_WITH_DOCS="$_TRUE" +_WITH_SYSTEMD_LOGIND="$_TRUE" +if [ $IS_ALPINE = 1 ]; then + _WITH_SYSTEMD_LOGIND="$_FALSE" +fi + +if [ -z "${NMTST_SEED_RAND+x}" ]; then + NMTST_SEED_RAND="$SRANDOM" + if [ -z "$NMTST_SEED_RAND" ]; then + NMTST_SEED_RAND="$(( ( (RANDOM<<15|RANDOM)<<15|RANDOM ) % 0xfffffffe ))" + fi +fi +export NMTST_SEED_RAND + +case "$CI" in + ""|"true"|"default"|"gitlab") + CI=default + ;; + *) + die "invalid \$CI \"$CI\"" + ;; +esac + +if [ "$CC" != gcc ]; then + _WITH_CRYPTO=nss +fi + +if [ "$WITH_DOCS" != "" ]; then + if _is_true "$WITH_DOCS"; then + _WITH_DOCS="$_TRUE" + else + _WITH_DOCS="$_FALSE" + fi +fi + +unset _WITH_VALGRIND_CHECKED +_with_valgrind() { + _is_true "$WITH_VALGRIND" 0 || return 1 + + test "$_WITH_VALGRIND_CHECKED" = "1" && return 0 + _WITH_VALGRIND_CHECKED=1 + + if [ "$IS_ALPINE" = 1 ]; then + # on Alpine we have no debug symbols and the suppressions + # don't work. Skip valgrind tests. + WITH_VALGRIND=0 + fi + + # Certain glib2 versions are known to report *lots* of leaks. Disable + # valgrind tests in this case. + # https://bugzilla.redhat.com/show_bug.cgi?id=1710417 + if grep -q '^PRETTY_NAME="Fedora 30 (.*)"$' /etc/os-release ; then + if rpm -q glib2 | grep -q glib2-2.60.2-1.fc30 ; then + WITH_VALGRIND=0 + fi + elif grep -q '^PRETTY_NAME="Fedora 31 (.*)"$' /etc/os-release; then + if rpm -q glib2 | grep -q glib2-2.61.0-2.fc31 ; then + WITH_VALGRIND=0 + fi + elif grep -q '^PRETTY_NAME="Debian.*sid"$' /etc/os-release; then + if dpkg -s libglib2.0-bin | grep -q '^Version: 2.66.4-2$' ; then + WITH_VALGRIND=0 + fi + fi + if [ "$WITH_VALGRIND" == 0 ]; then + echo "Don't use valgrind due to known issues in other packages." + return 1 + fi + return 0 +} + +############################################################################### + +_print_test_logs() { + echo ">>>> PRINT TEST LOGS $1 (start)" + if test -f test-suite.log; then + cat test-suite.log + fi + echo ">>>> PRINT TEST LOGS $1 (done)" + if _with_valgrind; then + echo ">>>> PRINT VALGRIND LOGS $1 (start)" + find -name '*.valgrind-log' -print0 | xargs -0 grep -H ^ || true + echo ">>>> PRINT VALGRIND LOGS $1 (done)" + fi +} + +run_autotools() { + NOCONFIGURE=1 ./autogen.sh + mkdir ./build + if [ "$_WITH_WERROR" == 1 ]; then + _WITH_WERROR_VAL="error" + else + _WITH_WERROR_VAL="yes" + fi + DISABLE_DEPENDENCY_TRACKING= + if [ $IS_ALPINE = 1 ]; then + DISABLE_DEPENDENCY_TRACKING='--disable-dependency-tracking' + fi + pushd ./build + ../configure \ + --prefix="$PWD/INST" \ + $DISABLE_DEPENDENCY_TRACKING \ + \ + --enable-introspection=$_WITH_DOCS \ + --enable-gtk-doc=$_WITH_DOCS \ + --with-systemd-logind=$_WITH_SYSTEMD_LOGIND \ + --enable-more-warnings="$_WITH_WERROR_VAL" \ + --enable-tests=yes \ + --with-crypto=$_WITH_CRYPTO \ + \ + --with-ebpf=no \ + \ + --with-iwd=yes \ + --with-ofono=yes \ + --enable-teamdctl=$_WITH_LIBTEAM \ + \ + --with-dhcpcanon=yes \ + --with-dhcpcd=yes \ + --with-dhclient=yes \ + \ + --with-netconfig=/bin/nowhere/netconfig \ + --with-resolvconf=/bin/nowhere/resolvconf \ + \ + --enable-ifcfg-rh=yes \ + --enable-ifupdown=yes \ + \ + #end + + if [ "$CONFIGURE_ONLY" != 1 ]; then + make -j 6 + make install + + export NM_TEST_CLIENT_CHECK_L10N=1 + + if ! make check -j 6 -k ; then + _print_test_logs "first-test" + echo ">>>> RUN SECOND TEST (start)" + NMTST_DEBUG="debug,TRACE,no-expect-message" make check -k || : + echo ">>>> RUN SECOND TEST (done)" + _print_test_logs "second-test" + die "autotools test failed" + fi + + if _with_valgrind; then + if ! NMTST_USE_VALGRIND=1 make check -j 3 -k ; then + _print_test_logs "(valgrind test)" + die "autotools+valgrind test failed" + fi + fi + fi + popd +} + +############################################################################### + +run_meson() { + if [ "$_WITH_WERROR" == 1 ]; then + _WITH_WERROR_VAL="--werror" + else + _WITH_WERROR_VAL="" + fi + meson setup build \ + \ + -Dprefix="$PWD/INST" \ + \ + --warnlevel 2 \ + $_WITH_WERROR_VAL \ + \ + -D ld_gc=false \ + -D session_tracking=no \ + -D systemdsystemunitdir=no \ + -D systemd_journal=false \ + -D selinux=false \ + -D libaudit=no \ + -D libpsl=false \ + -D vapi=false \ + -D introspection=$_WITH_DOCS \ + -D qt=false \ + -D crypto=$_WITH_CRYPTO \ + -D docs=$_WITH_DOCS \ + \ + -D ebpf=false \ + \ + -D iwd=true \ + -D ofono=true \ + -D teamdctl=$_WITH_LIBTEAM \ + \ + -D dhclient=/bin/nowhere/dhclient \ + -D dhcpcanon=/bin/nowhere/dhcpcanon \ + -D dhcpcd=/bin/nowhere/dhcpd \ + \ + -D netconfig=/bin/nowhere/netconfig \ + -D resolvconf=/bin/nowhere/resolvconf \ + \ + -D ifcfg_rh=false \ + -D ifupdown=true \ + \ + #end + + export NM_TEST_CLIENT_CHECK_L10N=1 + + if [ "$CONFIGURE_ONLY" != 1 ]; then + ninja -C build -v + ninja -C build install + + if ! meson test -C build -v --print-errorlogs ; then + echo ">>>> RUN SECOND TEST (start)" + NMTST_DEBUG="debug,TRACE,no-expect-message" \ + meson test -C build -v --print-errorlogs || : + echo ">>>> RUN SECOND TEST (done)" + die "meson test failed" + fi + + if _with_valgrind; then + if ! NMTST_USE_VALGRIND=1 meson test -C build -v --print-errorlogs ; then + _print_test_logs "(valgrind test)" + die "meson+valgrind test failed" + fi + fi + fi +} + +############################################################################### + +if [ "$BUILD_TYPE" == autotools ]; then + run_autotools +elif [ "$BUILD_TYPE" == meson ]; then + run_meson +fi + +if [ "$USE_CCACHE" = 1 ]; then + echo "ccache statistics:" + ccache -s +fi diff --git a/contrib/scripts/nm-code-format-container.sh b/contrib/scripts/nm-code-format-container.sh new file mode 100755 index 00000000..7a5ce0d4 --- /dev/null +++ b/contrib/scripts/nm-code-format-container.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e + +die() { + echo "$@" >&2 + exit 1 +} + +DIR="$(realpath "$(dirname "$0")/../../")" +cd "$DIR" + +# The correct clang-format version is the one from the Fedora version used in our +# gitlab-ci pipeline. Parse it from ".gitlab-ci/config.yml". +FEDORA_VERSION="$(sed '/^ tier: 1/,/^ - name/!d' .gitlab-ci/config.yml | sed -n "s/^ - '\([0-9]\+\)'$/\1/p" | sed -n 1p)" + +test -n "$FEDORA_VERSION" || die "Could not detect the Fedora version in .gitlab-ci/config.yml" + +IMAGENAME="nm-code-format:f$FEDORA_VERSION" + +ARGS=( "$@" ) + +if ! podman image exists "$IMAGENAME" ; then + echo "Building image \"$IMAGENAME\"..." + podman build \ + --squash-all \ + --tag "$IMAGENAME" \ + -f <(cat <<EOF +FROM fedora:$FEDORA_VERSION +RUN dnf upgrade -y +RUN dnf install -y git /usr/bin/clang-format +EOF +) +fi + +CMD=( ./contrib/scripts/nm-code-format.sh "${ARGS[@]}" ) + +podman run \ + --rm \ + --name "nm-code-format-f$FEDORA_VERSION" \ + -v "$DIR:/tmp/NetworkManager:Z" \ + -w /tmp/NetworkManager \ + -e "_NM_CODE_FORMAT_CONTAINER=$IMAGENAME" \ + -ti \ + "$IMAGENAME" \ + "${CMD[@]}" diff --git a/contrib/scripts/nm-code-format.sh b/contrib/scripts/nm-code-format.sh new file mode 100755 index 00000000..3c18cd77 --- /dev/null +++ b/contrib/scripts/nm-code-format.sh @@ -0,0 +1,222 @@ +#!/bin/bash + +set -e + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +EXCLUDE_PATHS_TOPLEVEL=( + "src/c-list" + "src/c-rbtree" + "src/c-siphash" + "src/c-stdaux" + "src/libnm-std-aux/unaligned-fundamental.h" + "src/libnm-std-aux/unaligned.h" + "src/libnm-systemd-core/src" + "src/libnm-systemd-shared/src" + "src/linux-headers" + "src/n-acd" + "src/n-dhcp4" +) + +NM_ROOT="$(git rev-parse --show-toplevel)" || die "not inside a git repository" +NM_PREFIX="$(git rev-parse --show-prefix)" || die "not inside a git repository" + +if [ ! -f "$NM_ROOT/.clang-format" ]; then + die "Error: the clang-format file in \"$NM_ROOT\" does not exist" +fi + +if ! command -v clang-format &> /dev/null; then + die "Error: clang-format is not installed. On RHEL/Fedora/CentOS run 'dnf install clang-tools-extra'" +fi + +if test -n "$NM_PREFIX"; then + EXCLUDE_PATHS=() + for e in "${EXCLUDE_PATHS_TOPLEVEL[@]}"; do + REGEX="^$NM_PREFIX([^/].*)$" + if [[ "$e" =~ $REGEX ]]; then + EXCLUDE_PATHS+=("${BASH_REMATCH[1]}") + fi + done +else + EXCLUDE_PATHS=("${EXCLUDE_PATHS_TOPLEVEL[@]}") +fi + +FILES=() +HAS_EXPLICIT_FILES=0 +SHOW_FILENAMES=0 +TEST_ONLY=0 +CHECK_UPSTREAM= + +usage() { + printf "Usage: %s [OPTION]... [FILE]...\n" "$(basename "$0")" + printf "Reformat source files using NetworkManager's code-style.\n\n" + printf "If no file is given the script runs on the whole codebase.\n" + printf "OPTIONS:\n" + printf " -h Print this help message.\n" + printf " -i Reformat files (the default).\n" + printf " -n|--dry-run Only check the files (contrary to \"-i\").\n" + printf " -a|--all Check all files (the default).\n" + printf " -u|--upstream COMMIT Check only files from \`git diff --name-only COMMIT\` (contrary to \"-a\").\n" + printf " This also affects directories given in the [FILE] list, but not files.\n" + printf " If this is the last parameter and COMMIT is unspecified/empty, it defaults to \"main\".\n" + printf " -F|--fast Same as \`-u HEAD^\`.\n" + printf " --show-filenames Only print the filenames that would be checked/formatted\n" + printf " -- Separate options from filenames/directories\n" + if [ -n "${_NM_CODE_FORMAT_CONTAINER+x}" ] ; then + printf "\n" + printf "Command runs inside container image \"$_NM_CODE_FORMAT_CONTAINER\".\n" + printf "Delete/renew image with \`podman rmi \"$_NM_CODE_FORMAT_CONTAINER\"\`.\n" + fi +} + +ls_files_exist() { + local OLD_IFS="$IFS" + local f + + IFS=$'\n' + for f in $(cat) ; do + test -f "$f" && printf '%s\n' "$f" + done + IFS="$OLD_IFS" +} + +ls_files_filter() { + local OLD_IFS="$IFS" + local f + + IFS=$'\n' + for f in $(cat) ; do + local found=1 + local p + for p; do + [[ "$f" = "$p/"* ]] && found= + [[ "$f" = "$p" ]] && found= + done + test -n "$found" && printf '%s\n' "$f" + done + IFS="$OLD_IFS" +} + +g_ls_files() { + local pattern="$1" + shift + + if [ -z "$CHECK_UPSTREAM" ]; then + git ls-files -- "$pattern" + else + git diff --no-renames --name-only "$CHECK_UPSTREAM" -- "$pattern" \ + | ls_files_exist + fi | ls_files_filter "$@" +} + +HAD_DASHDASH=0 +while (( $# )); do + if [ "$HAD_DASHDASH" = 0 ]; then + case "$1" in + -h) + usage + exit 0 + ;; + --show-filenames) + SHOW_FILENAMES=1 + shift + continue + ;; + -a|--all) + CHECK_UPSTREAM= + shift + continue + ;; + -u|--upstream) + shift + CHECK_UPSTREAM="$1" + test -n "$CHECK_UPSTREAM" || CHECK_UPSTREAM=main + shift || : + continue + ;; + -F|--fast) + CHECK_UPSTREAM='HEAD^' + shift + continue + ;; + -n|--dry-run) + TEST_ONLY=1 + shift + continue + ;; + -i) + TEST_ONLY=0 + shift + continue + ;; + --) + HAD_DASHDASH=1 + shift + continue + ;; + esac + fi + if [ -d "$1" ]; then + while IFS='' read -r line; + do FILES+=("$line") + done < <(CHECK_UPSTREAM="$CHECK_UPSTREAM" g_ls_files "${1}/*.[hc]" "${EXCLUDE_PATHS[@]}") + elif [ -f "$1" ]; then + FILES+=("$1") + else + usage >&2 + echo >&2 + die "Unknown argument \"$1\" which also is neither a file nor a directory." + fi + shift + HAS_EXPLICIT_FILES=1 +done + +if [ $HAS_EXPLICIT_FILES = 0 ]; then + while IFS='' read -r line; do + FILES+=("$line") + done < <(CHECK_UPSTREAM="$CHECK_UPSTREAM" g_ls_files '*.[ch]' "${EXCLUDE_PATHS[@]}") +fi + +if [ $SHOW_FILENAMES = 1 ]; then + for f in "${FILES[@]}" ; do + printf '%s\n' "$f" + done + exit 0 +fi + +if [ "${#FILES[@]}" = 0 ]; then + if [ -z "$CHECK_UPSTREAM" ]; then + die "Error: no files to check" + fi + exit 0 +fi + +FLAGS_TEST=( --Werror -n --ferror-limit=1 ) + +if [ $TEST_ONLY = 1 ]; then + # We assume that all formatting is correct. In that mode, passing + # all filenames to clang-format is significantly faster. + # + # Only in case of an error, we iterate over the files one by one + # until we find the first invalid file. + for f in "${FILES[@]}"; do + [ -f "$f" ] || die "Error: file \"$f\" does not exist (or is not a regular file)" + done + clang-format "${FLAGS_TEST[@]}" "${FILES[@]}" &>/dev/null && exit 0 + for f in "${FILES[@]}"; do + [ -f "$f" ] || die "Error: file \"$f\" does not exist (or is not a regular file)" + if ! clang-format "${FLAGS_TEST[@]}" "$f" &>/dev/null; then + FF="$(mktemp)" + trap 'rm -f "$FF"' EXIT + clang-format "$f" 2>/dev/null > "$FF" + git --no-pager diff "$f" "$FF" || : + die "Error: file \"$f\" has style issues."$'\n'"Fix it by running \`\"$0\" -i \"$f\"\` using $(clang-format --version)" + fi + done + die "an unknown error happened." +fi + +clang-format -i "${FILES[@]}" diff --git a/contrib/scripts/nm-copr-build-nm-git-bundle.sh b/contrib/scripts/nm-copr-build-nm-git-bundle.sh new file mode 100755 index 00000000..5447a56e --- /dev/null +++ b/contrib/scripts/nm-copr-build-nm-git-bundle.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# create a nm-git-bundle.git bundle and a SRPM for building it +# as a package. This bundle contains the current git history +# of upstream NetworkManager. +# +# The sole purpose of this is to fetch from the bundle to save +# downloading the entire upstream git repository of NetworkManager. +# +# This script is also used by [1] to generate the SRPM. +# [1] https://copr.fedorainfracloud.org/coprs/networkmanager/NetworkManager-main/package/nm-git-bundle/ +# +# The purpose is the following. We build (many) NetworkManager packages in +# copr. The build process runs a script (contrib/scripts/nm-copr-build.sh) that +# fetches the git repository (and we cannot just do a shallow copy -- because +# the version number is calculated by counts all the commits in the HEAD's +# history). NetworkManager's git repository is relatively large so fetching it +# over and over is wasteful. The idea is to have a recent git-bundle of the +# repository, which is hosted close-by in the copr infrastructure. So the build +# script first tries to download the bundle to get the bulk of the git history, +# before doing additional fetches from the upstream repository. From time to +# time, a new bundle has to be generated in copr. + +set -ex + +if [ -z "$GIT_URL" ]; then + GIT_URL=https://github.com/NetworkManager/NetworkManager + #GIT_URL=https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git +fi + +git clone -n "$GIT_URL" + +pushd NetworkManager + +REFS=( + $(git branch -a | sed -n 's#^ *remotes/origin/\(main\|nm-1-[0-9]\+\)$#\1#p') +) + +unset R +unset H +for R in "${REFS[@]}"; do + H="$(git show-ref --verify --hash "refs/remotes/origin/$R")" + git update-ref "refs/heads/$R" "$H" +done + +git bundle create nm-git-bundle.git "${REFS[@]}" + +popd + +DIR="$(mktemp -d rpmbuild.XXXXXX)" + +mkdir -p "$DIR/SOURCES" +mkdir -p "$DIR/SPECS" + +cat <<EOF > "$DIR/SPECS/nm-git-bundle.spec" +Name: nm-git-bundle +Version: $(date '+%Y%m%d') +Release: $(date '+%H%M%S') +Summary: git-bundle of NetworkManager upstream repository + +License: Public Domain +URL: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/tree/main/contrib/fedora/rpm/nm-git-bundle.spec + +%global GIT_URL 'https://github.com/NetworkManager/NetworkManager' +#global GIT_URL 'https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git' + +Source0: nm-git-bundle.git + + +BuildArch: noarch + + +%description +A git-bundle of NetworkManager upstream git repository. Useful to safe +fetching the entire repository from the internet. + + +%install +mkdir -p %{buildroot}/usr/share/NetworkManager/ +cp %{SOURCE0} %{buildroot}/usr/share/NetworkManager/ + + +%files +/usr/share/NetworkManager/nm-git-bundle.git +EOF + +mv ./NetworkManager/nm-git-bundle.git "$DIR/SOURCES/" + +rpmbuild --define "_topdir $DIR" -bs "$DIR/SPECS/nm-git-bundle.spec" + +mv "$DIR/SRPMS/"nm-git-bundle-*.src.rpm . +mv "$DIR/SPECS/nm-git-bundle.spec" . +mv "$DIR/SOURCES/nm-git-bundle.git" . +rm -rf "$DIR" + diff --git a/contrib/scripts/nm-copr-build.sh b/contrib/scripts/nm-copr-build.sh new file mode 100755 index 00000000..94c804fb --- /dev/null +++ b/contrib/scripts/nm-copr-build.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# This is the build script used by our copr repository at +# https://copr.fedorainfracloud.org/coprs/networkmanager +# +# On a new upstream release, add new copr jobs named "NetworkManager-X.Y" and +# "NetworkManager-X.Y-debug". +# +# - best, look at the latest copr project and replicate the settings. +# - add a custom build with the following script: +# +# #!/bin/bash +# export GIT_REF=nm-$X-$Y +# export DEBUG=0/1 +# export LTO= +# curl https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/raw/main/contrib/scripts/nm-copr-build.sh | bash +# +# - for certain CentOS/EPEL you need to add https://copr.fedorainfracloud.org/coprs/nmstate/nm-build-deps/ +# as build chroot. See under "Settings/Project Details" for the latest copr project. +# - go to "Settings/Integrations" and find the notification URL for the project. Then +# go to https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/hooks and add +# a push event for the "nm-$X-$Y" branch. +# +# environment variables for this script: +# - GIT_REF: the ref that should be build. Can be "main" or a git sha. +# - DEBUG: set to 1 to build "--with debug". Otherwise the default is a release +# build. +# - LTO: set to 1/0 to build "--with/--without lto", otherwise the default depends +# on the distribution. +# - NM_GIT_BUNDLE: set to a HTTP url where to fetch the nm-git-bundle-*.noarch.rpm +# from. Set to empty to skip it. By default, it fetches the bundle from copr. +# See "contrib/scripts/nm-copr-build-nm-git-bundle.sh" script and +# https://copr.fedorainfracloud.org/coprs/networkmanager/NetworkManager-main/package/nm-git-bundle/ + +set -ex + +if [[ "$DEBUG" == 1 ]]; then + DEBUG="--with debug" +else + DEBUG="--without debug" +fi + +if [ "$LTO" = 0 ]; then + LTO='--without lto' +elif [ "$LTO" = 1 ]; then + LTO='--with lto' +else + LTO= +fi + +if [[ -z "$GIT_REF" ]]; then + echo "\$GIT_REF is not set!" + exit 1 +fi + +mkdir NetworkManager +pushd NetworkManager +git init . + +git remote add origin https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git +git remote add --no-tags github https://github.com/NetworkManager/NetworkManager + +get_nm_git_bundle() { + # try to fetch the refs from nm-git-bundle. + # + # This script runs in copr infrastructure to create the SRPM. + # The idea is that this URL is close and downloading it is cheaper + # than fetching everything from upstream git. + if [ -z "$NM_GIT_BUNDLE" ]; then + if [ -n "${NM_GIT_BUNDLE+x}" ]; then + return 0 + fi + NM_GIT_BUNDLE='https://download.copr.fedorainfracloud.org/results/networkmanager/NetworkManager-main/fedora-38-x86_64/06008259-nm-git-bundle/nm-git-bundle-20230606-102458.noarch.rpm' + fi + mkdir nm-git-bundle + pushd nm-git-bundle + time curl "$NM_GIT_BUNDLE" \ + | rpm2cpio - \ + | cpio -idmv + popd + git remote add nm-git-bundle "$PWD/nm-git-bundle/usr/share/NetworkManager/nm-git-bundle.git" + git fetch nm-git-bundle +} + +get_nm_git_bundle +git fetch github +git fetch origin +git remote remove nm-git-bundle || true + +GIT_SHA="$(git show-ref --verify --hash "$GIT_REF" 2>/dev/null || + git show-ref --verify --hash "refs/remotes/origin/$GIT_REF" 2>/dev/null || + git rev-parse --verify "refs/remotes/origin/$GIT_REF" 2>/dev/null || + git rev-parse --verify "$GIT_REF^{commit}" 2>/dev/null)" + +git checkout -b tmp "$GIT_SHA" + +./contrib/fedora/rpm/build_clean.sh -g -S -w test $DEBUG $LTO -s copr +popd + +mv ./NetworkManager/contrib/fedora/rpm/latest/{SOURCES,SPECS}/* . +rm -rf ./NetworkManager diff --git a/contrib/scripts/nm-import-openconnect b/contrib/scripts/nm-import-openconnect new file mode 100755 index 00000000..f14895d3 --- /dev/null +++ b/contrib/scripts/nm-import-openconnect @@ -0,0 +1,261 @@ +#!/usr/bin/env lua +-- SPDX-License-Identifier: GPL-2.0-or-later +-- +-- Copyright (C) 2015 Red Hat, Inc. +-- + +-- Script for importing/converting OpenConnect VPN configuration files for NetworkManager +-- In general, the implementation follows the logic of import() from +-- https://git.gnome.org/browse/network-manager-openconnect/tree/properties/nm-openconnect.c + +---------------------- +-- Helper functions -- +---------------------- +function read_all(in_file) + local f, msg = io.open(in_file, "r") + if not f then return nil, msg; end + local content = f:read("*all") + f:close() + return content +end + +function uuid() + math.randomseed(os.time()) + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + local uuid = string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end) + return uuid +end + +function vpn_settings_to_text(vpn_settings) + local t = {} + for k,v in pairs(vpn_settings) do + t[#t+1] = k.."="..v + end + return table.concat(t, "\n") +end + +function usage() + local basename = string.match(arg[0], '[^/\\]+$') or arg[0] + print(basename .. " - convert/import OpenConnect VPN configuration to NetworkManager") + print("Usage:") + print(" " .. basename .. " <input-file> <output-file>") + print(" - converts OpenConnect VPN config to NetworkManager keyfile") + print("") + print(" " .. basename .. " --import <input-file1> <input-file2> ...") + print(" - imports OpenConnect VPN config(s) to NetworkManager") + os.exit(1) +end + + +------------------------------------------- +-- Functions for VPN options translation -- +------------------------------------------- +function handle_yes(t, option, value) + t[option] = "yes" +end +function handle_generic(t, option, value) + if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) end + t[option] = value[2] +end + +-- global variables +g_con_data = {} +g_vpn_data = {} + +vpn2nm = { + ["Description"] = { nm_opt="id", func=handle_generic, tbl=g_con_data }, + ["Host"] = { nm_opt="gateway", func=handle_generic, tbl=g_vpn_data }, + ["CACert"] = { nm_opt="cacert", func=handle_generic, tbl=g_vpn_data }, + ["Proxy"] = { nm_opt="proxy", func=handle_generic, tbl=g_vpn_data }, + ["CSDEnable"] = { nm_opt="enable_csd_trojan", func=handle_yes, tbl=g_vpn_data }, + ["CSDWrapper"] = { nm_opt="csd_wrapper", func=handle_generic, tbl=g_vpn_data }, + ["UserCertificate"] = { nm_opt="usercert", func=handle_generic, tbl=g_vpn_data }, + ["PrivateKey"] = { nm_opt="userkey", func=handle_generic, tbl=g_vpn_data }, + ["FSID"] = { nm_opt="pem_passphrase_fsid", func=handle_yes, tbl=g_vpn_data }, + ["StokenSource"] = { nm_opt="stoken_source", func=handle_generic, tbl=g_vpn_data }, + ["StokenString"] = { nm_opt="stoken_string", func=handle_generic, tbl=g_vpn_data }, +} + +------------------------------------------------------ +-- Read and convert the config into the global vars -- +------------------------------------------------------ +function read_and_convert(in_file) + local function line_split(str) + -- split at '=' character + local sep, fields = "=", {} + local pattern = string.format("([^%s]+)%s(.+)", sep, sep) + fields[1], fields[2] = str:match(pattern) + return fields + end + + in_text, msg = read_all(in_file) + if not in_text then return false, msg end + + -- loop through the config and convert it + for line in in_text:gmatch("[^\r\n]+") do + repeat + -- skip comments and empty lines + if line:find("^%s*[#;]") or line:find("^%s*$") then break end + -- trim leading and trailing spaces + line = line:find("^%s*$") and "" or line:match("^%s*(.*%S)") + + local words = line_split(line) + local val = vpn2nm[words[1]] + if val then + if type(val) == "table" then val.func(val.tbl, val.nm_opt, words) + else print(string.format("debug: '%s' : val=%s"..val)) end + end + until true + end + + -- check mandatory parameters + if not g_vpn_data["gateway"] then + local msg = in_file .. ": Not a valid OpenConnect VPN configuration" + return false, msg + end + return true +end + +-------------------------------------------------------- +-- Create and write connection file in keyfile format -- +-------------------------------------------------------- +function write_vpn_to_keyfile(in_file, out_file) + connection = [[ +[connection] +id=__NAME_PLACEHOLDER__ +uuid=__UUID_PLACEHOLDER__ +type=vpn +autoconnect=no + +[ipv4] +method=auto +never-default=true + +[ipv6] +method=auto + +[vpn] +service-type=org.freedesktop.NetworkManager.openconnect +]] + + connection = connection .. vpn_settings_to_text(g_vpn_data) + + local con_name = g_con_data["id"] or (out_file:gsub(".*/", "")) + connection = string.gsub(connection, "__NAME_PLACEHOLDER__", con_name) + connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid()) + + -- write output file + local f, err = io.open(out_file, "w") + if not f then io.stderr:write(err) return false end + f:write(connection) + f:close() + + local ofname = out_file:gsub(".*/", "") + io.stderr:write("Successfully converted VPN configuration: " .. in_file .. " => " .. out_file .. "\n") + io.stderr:write("To use the connection, do:\n") + io.stderr:write("# cp " .. out_file .. " /etc/NetworkManager/system-connections\n") + io.stderr:write("# chmod 600 /etc/NetworkManager/system-connections/" .. ofname .. "\n") + io.stderr:write("# nmcli con load /etc/NetworkManager/system-connections/" .. ofname .. "\n") + return true +end + +--------------------------------------------- +-- Import VPN connection to NetworkManager -- +--------------------------------------------- +function import_vpn_to_NM(filename) + local lgi = require 'lgi' + local GLib = lgi.GLib + local NM = lgi.NM + + -- function creating NMConnection + local function create_profile(name) + local profile = NM.SimpleConnection.new() + + s_con = NM.SettingConnection.new() + s_vpn = NM.SettingVpn.new() + s_con[NM.SETTING_CONNECTION_ID] = name + s_con[NM.SETTING_CONNECTION_UUID] = uuid() + s_con[NM.SETTING_CONNECTION_TYPE] = "vpn" + s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.openconnect" + for k,v in pairs(g_vpn_data) do + s_vpn:add_data_item(k, v) + end + + profile:add_setting(s_con) + profile:add_setting(s_vpn) + return profile + end + + -- callback function for add_connection() + local function added_cb(client, result, data) + local con,err,code = client:add_connection_finish(result) + if con then + print(string.format("%s: Imported to NetworkManager: %s - %s", + filename, con:get_uuid(), con:get_id())) + else + io.stderr:write(code .. ": " .. err .. "\n"); + return false + end + main_loop:quit() + end + + local profile_name = g_con_data["id"] or string.match(filename, '[^/\\]+$') or filename + main_loop = GLib.MainLoop(nil, false) + local con = create_profile(profile_name) + local client = NM.Client.new() + + -- send the connection to NetworkManager + client:add_connection_async(con, true, nil, added_cb, nil) + + -- run main loop so that the callback could be called + main_loop:run() + return true +end + + +--------------------------- +-- Main code starts here -- +--------------------------- +local import_mode = false +local infile, outfile + +-- parse command-line arguments +if not arg[1] or arg[1] == "--help" or arg[1] == "-h" then usage() end +if arg[1] == "--import" or arg[1] == "-i" then + infile = arg[2] + if not infile then usage() end + import_mode = true +else + infile = arg[1] + outfile = arg[2] + if not infile or not outfile then usage() end + if arg[3] then usage() end +end + +if import_mode then + -- check if lgi is available + local success,msg = pcall(require, 'lgi') + if not success then + io.stderr:write("Lua lgi module is not available, please install it (usually lua-lgi package)\n") + -- print(msg) + os.exit(1) + end + -- read configs, convert them and import to NM + for i = 2, #arg do + ok, err_msg = read_and_convert(arg[i]) + if ok then import_vpn_to_NM(arg[i]) + else io.stderr:write(err_msg .. "\n") end + -- reset global vars + g_con_data = {} + g_vpn_data = {} + end +else + -- read configs, convert them and write as NM keyfile connection + ok, err_msg = read_and_convert(infile) + if ok then write_vpn_to_keyfile(infile, outfile) + else io.stderr:write(err_msg .. "\n") end +end + diff --git a/contrib/scripts/nm-import-openvpn b/contrib/scripts/nm-import-openvpn new file mode 100755 index 00000000..6c9f39c4 --- /dev/null +++ b/contrib/scripts/nm-import-openvpn @@ -0,0 +1,543 @@ +#!/usr/bin/env lua +-- SPDX-License-Identifier: GPL-2.0-or-later +-- +-- Copyright (C) 2015 Red Hat, Inc. +-- + +-- Script for importing/converting OpenVPN configuration files for NetworkManager +-- In general, the implementation follows the logic of import() from +-- https://git.gnome.org/browse/network-manager-openvpn/tree/properties/import-export.c + + +---------------------- +-- Helper functions -- +---------------------- +function read_all(in_file) + local f, msg = io.open(in_file, "r") + if not f then return nil, msg; end + local content = f:read("*all") + f:close() + return content +end + +function uuid() + math.randomseed(os.time()) + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + local uuid = string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end) + return uuid +end + +function unquote(str) + return (string.gsub(str, "^([\"\'])(.*)%1$", "%2")) +end + +function parse_ipv4_to_bytes(ip_addr) + local b1,b2,b3,b4 = ip_addr:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$") + b1 = tonumber(b1) + b2 = tonumber(b2) + b3 = tonumber(b3) + b4 = tonumber(b4) + return b1, b2, b3, b4 +end + +function is_ipv4(ip_addr) + local b1,b2,b3,b4 = parse_ipv4_to_bytes(ip_addr) + if not b1 or (b1 > 255) then return false end + if not b2 or (b2 > 255) then return false end + if not b3 or (b3 > 255) then return false end + if not b4 or (b4 > 255) then return false end + return true +end + +function ip_mask_to_prefix(mask) + local b, prefix + local b1,b2,b3,b4 = parse_ipv4_to_bytes(mask) + + if b4 ~= 0 then + prefix = 24 + b = b4 + elseif b3 ~= 0 then + prefix = 16 + b = b3 + elseif b2 ~= 0 then + prefix = 8 + b = b2 + else + prefix = 0 + b = b1 + end + while b ~= 0 do + prefix = prefix + 1 + b = bit32.band(0x000000FF, bit32.lshift(b, 1)) + end + return prefix +end + +function vpn_settings_to_text(vpn_settings) + local t = {} + for k,v in pairs(vpn_settings) do + t[#t+1] = k.."="..v + end + return table.concat(t, "\n") +end + +function usage() + local basename = string.match(arg[0], '[^/\\]+$') or arg[0] + print(basename .. " - convert/import OpenVPN configuration to NetworkManager") + print("Usage:") + print(" " .. basename .. " <input-file> <output-file>") + print(" - converts OpenVPN config to NetworkManager keyfile") + print("") + print(" " .. basename .. " --import <input-file1> <input-file2> ...") + print(" - imports OpenVPN config(s) to NetworkManager") + os.exit(1) +end + + +------------------------------------------- +-- Functions for VPN options translation -- +------------------------------------------- +function set_bool(t, option, value) + g_switches[option] = true +end +function handle_yes(t, option, value) + t[option] = "yes" +end +function handle_generic(t, option, value) + if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end + t[option] = value[2] +end +function handle_generic_unquote(t, option, value) + if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end + t[option] = unquote(value[2]) +end +function handle_number(t, option, value) + if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end + if not tonumber(value[2]) then + io.stderr:write(string.format("Warning: ignoring not numeric value '%s' for option '%s'\n", value[2], value[1])) + return + end + t[option] = value[2] +end +function handle_proto(t, option, value) + if not value[2] then io.stderr:write("Warning: ignoring invalid option 'proto'\n") end + if value[2] == "tcp" or value[3] == "tcp-client" or value[2] == "tcp-server" then + t[option] = "yes" + end +end +function handle_comp_lzo(t, option, value) + value[2] = value[2] or "adaptive" + if value[2] == "no" then + value[2] = "no-by-default" + elseif value[2] ~= "yes" and value[2] ~= "adaptive" then + io.stderr:write(string.format("Warning: ignoring invalid argument '%s' in option 'comp-lzo'\n", value[2])) + return + end + t[option] = value[2] +end +function handle_dev_type(t, option, value) + if value[2] ~= "tun" and value[2] ~= "tap" then + io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) + end + t[option] = value[2] +end +function handle_remote(t, option, value) + local rem + if not value[2] then io.stderr:write("Warning: ignoring invalid option 'remote'\n") return end + rem = value[2] + if tonumber(value[3]) then + rem = rem .. ":" .. value[3] + end + if value[4] == "udp" or value[4] == "tcp" then + rem = rem .. ":" .. value[4] + end + if t[option] then + t[option] = t[option] .. " " .. rem + else + t[option] = rem + end + g_switches[value[1]] = true +end +function handle_port(t, option, value) + if tonumber(value[2]) then + t[option] = value[2] + end +end +function handle_proxy(t, option, value) + if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end + if value[4] then io.stderr:write(string.format("Warning: the third argument of '%s' is not supported yet\n", value[1])) end + t[option[1]] = string.gsub(value[1], "-proxy", "") + t[option[2]] = value[2] + t[option[3]] = value[3] +end +function handle_ifconfig(t, option, value) + if not (value[2] and value[3]) then io.stderr:write("Warning: ignoring invalid option 'ifconfig'\n") return end + t[option[1]] = value[2] + t[option[2]] = value[3] +end +function handle_keepalive(t, option, value) + if (not (value[2] and value[3])) or (not tonumber(value[2]) or not tonumber(value[3])) then + io.stderr:write("Warning: ignoring invalid option 'keepalive'; two numbers required\n") + return + end + t[option[1]] = value[2] + t[option[2]] = value[3] +end +function handle_path(t, option, value) + if value[1] == "pkcs12" then + t["ca"] = value[2] + t["cert"] = value[2] + t["key"] = value[2] + else + t[option] = value[2] + end +end +function handle_secret(t, option, value) + t[option[1]] = value[2] + t[option[2]] = value[3] + g_switches[value[1]]= true +end +function handle_remote_cert_tls(t, option, value) + if value[2] ~= "client" and value[2] ~= "server" then + io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) + return + end + t[option] = value[2] +end +function handle_routes(t, option, value) + if not value[2] then io.stderr:write("Warning: invalid option 'route'\n") return end + netmask = (value[3] and value[3] ~= "default") and value[3] or "255.255.255.255" + gateway = (value[4] and value[4] ~= "default") and value[4] or "0.0.0.0" + metric = (value[5] and value[5] ~= "default") and value[5] or "0" + + if not is_ipv4(value[2]) then + if value[2] == "vpn_gateway" or value[2] == "net_gateway" or value[2] == "remote_host" then + io.stderr:write(string.format("Warning: sorry, the '%s' keyword is not supported by NetworkManager in option '%s'\n", + value[2], value[1])) + else + io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 address in option '%s'\n", value[2], value[1])) + end + return + end + if not is_ipv4(netmask) then + io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 netmask in option '%s'\n", netmask, value[1])) + return + end + if not is_ipv4(gateway) then + if gateway == "vpn_gateway" or gateway == "net_gateway" or gateway == "remote_host" then + io.stderr:write(string.format("Warning: sorry, the '%s' keyword is not supported by NetworkManager in option '%s'\n", + gateway, value[1])) + else + io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 gateway in option '%s'\n", gateway, value[1])) + end + return + end + if not tonumber(metric) then + io.stderr:write(string.format("Warning: '%s' is not a valid metric in option '%s'\n", metric, value[1])) + return + end + + if not t[option] then t[option] = {} end + t[option][#t[option]+1] = {value[2], netmask, gateway, metric} +end +function handle_verify_x509_name(t, option, value) + if not value[2] then io.stderr:write("Warning: missing argument in option 'verify-x509-name'\n") return end + value[2] = unquote(value[2]) + value[3] = value[3] or "subject" + if value[3] ~= "subject" and value[3] ~= "name" and value[3] ~= "name-prefix" then + io.stderr:write(string.format("Warning: ignoring invalid value '%s' for type in option '%s'\n", value[3], value[1])) + return + end + t[option] = value[3] .. ":" .. value[2] +end + +-- global variables +g_vpn_data = {} +g_ip4_data = {} +g_switches = {} + +vpn2nm = { + ["auth"] = { nm_opt="auth", func=handle_generic, tbl=g_vpn_data }, + ["auth-user-pass"] = { nm_opt="auth-user-pass", func=set_bool, tbl={} }, + ["ca"] = { nm_opt="ca", func=handle_path, tbl=g_vpn_data }, + ["cert"] = { nm_opt="cert", func=handle_path, tbl=g_vpn_data }, + ["cipher"] = { nm_opt="cipher", func=handle_generic, tbl=g_vpn_data }, + ["client"] = { nm_opt="client", func=set_bool, tbl={} }, + ["comp-lzo"] = { nm_opt="comp-lzo", func=handle_comp_lzo, tbl=g_vpn_data }, + ["dev"] = { nm_opt="dev", func=handle_generic, tbl=g_vpn_data }, + ["dev-type"] = { nm_opt="dev-type", func=handle_dev_type, tbl=g_vpn_data }, + ["float"] = { nm_opt="float", func=handle_yes, tbl=g_vpn_data }, + ["fragment"] = { nm_opt="fragment-size", func=handle_generic, tbl=g_vpn_data }, + ["http-proxy"] = { nm_opt={"proxy-type", "proxy-server", "proxy-port"}, func=handle_proxy, tbl=g_vpn_data }, + ["http-proxy-retry"] = { nm_opt="proxy-retry", func=handle_yes, tbl=g_vpn_data }, + ["ifconfig"] = { nm_opt={"local-ip", "remote-ip"}, func=handle_ifconfig, tbl=g_vpn_data }, + ["keepalive"] = { nm_opt={"ping", "ping-restart"}, func=handle_keepalive, tbl=g_vpn_data }, + ["key"] = { nm_opt="key", func=handle_path, tbl=g_vpn_data }, + ["keysize"] = { nm_opt="keysize", func=handle_generic, tbl=g_vpn_data }, + ["max-routes"] = { nm_opt="max-routes", func=handle_number, tbl=g_vpn_data }, + ["mssfix"] = { nm_opt="mssfix", func=handle_yes, tbl=g_vpn_data }, + ["ns-cert-type"] = { nm_opt="ns-cert-type", func=handle_remote_cert_tls, tbl=g_vpn_data }, + ["ping"] = { nm_opt="ping", func=handle_number, tbl=g_vpn_data }, + ["ping-exit"] = { nm_opt="ping-exit", func=handle_number, tbl=g_vpn_data }, + ["ping-restart"] = { nm_opt="ping-restart", func=handle_number, tbl=g_vpn_data }, + ["pkcs12"] = { nm_opt="client", func=handle_path, tbl=g_vpn_data }, + ["port"] = { nm_opt="port", func=handle_port, tbl=g_vpn_data }, + ["proto"] = { nm_opt="proto-tcp", func=handle_proto, tbl=g_vpn_data }, + ["remote"] = { nm_opt="remote", func=handle_remote, tbl=g_vpn_data }, + ["remote-cert-tls"] = { nm_opt="remote-cert-tls", func=handle_remote_cert_tls, tbl=g_vpn_data }, + ["remote-random"] = { nm_opt="remote-random", func=handle_yes, tbl=g_vpn_data }, + ["reneg-sec"] = { nm_opt="reneg-seconds", func=handle_generic, tbl=g_vpn_data }, + ["route"] = { nm_opt="routes", func=handle_routes, tbl=g_ip4_data }, + ["rport"] = { nm_opt="port", func=handle_port, tbl=g_vpn_data }, + ["secret"] = { nm_opt={"static-key", "static-key-direction"}, func=handle_secret, tbl=g_vpn_data }, + ["socks-proxy"] = { nm_opt={"proxy-type", "proxy-server", "proxy-port"}, func=handle_proxy, tbl=g_vpn_data }, + ["socks-proxy-retry"] = { nm_opt="proxy-retry", func=handle_yes, tbl=g_vpn_data }, + ["tls-auth"] = { nm_opt={"ta", "ta-dir"}, func=handle_secret, tbl=g_vpn_data }, + ["tls-cipher"] = { nm_opt="tls-cipher", func=handle_generic_unquote, tbl=g_vpn_data }, + ["tls-client"] = { nm_opt="client", func=set_bool, tbl={} }, + ["tls-remote"] = { nm_opt="tls-remote", func=handle_generic_unquote, tbl=g_vpn_data }, + ["tun-ipv6"] = { nm_opt="tun-ipv6", func=handle_yes, tbl=g_vpn_data }, + ["tun-mtu"] = { nm_opt="tunnel-mtu", func=handle_generic, tbl=g_vpn_data }, + ["verify-x509-name"] = { nm_opt="verify-x509-name", func=handle_verify_x509_name,tbl=g_vpn_data }, +} + +------------------------------------------------------------ +-- Read and convert the config into the global g_vpn_data -- +----------------------------------------------------------- +function read_and_convert(in_file) + local function line_split(line) + local t={} + local i, idx = 1, 1 + local delim = "\"" + while true do + local a,b = line:find("%S+", idx) + if not a then break end + + local str = line:sub(a,b) + local quote = nil + if str:sub(1,1) == delim and str:sub(#str,#str) ~= delim then + quote = (line.." "):find(delim.."%s", b + 1) + end + + if quote then + t[i] = line:sub(a, quote) + idx = quote + 1 + else + t[i] = str + idx = b + 1 + end + i = i + 1 + end + return t + end + + in_text, msg = read_all(in_file) + if not in_text then return false, msg end + + -- loop through the config and convert it + for line in in_text:gmatch("[^\r\n]+") do + repeat + -- skip comments and empty lines + if line:find("^%s*[#;]") or line:find("^%s*$") then break end + -- trim leading and trailing spaces + line = line:find("^%s*$") and "" or line:match("^%s*(.*%S)") + + local words = line_split(line) + local val = vpn2nm[words[1]] + if val then + if type(val) == "table" then val.func(val.tbl, val.nm_opt, words) + else print(string.format("debug: '%s' : val=%s"..val)) end + end + until true + end + + -- check some inter-option dependencies + if not g_switches["client"] and not g_switches["secret"] then + local msg = in_file .. ": Not a valid OpenVPN client configuration" + return false, msg + end + if not g_switches["remote"] then + local msg = in_file .. ": Not a valid OpenVPN configuration (no remote)" + return false, msg + end + + -- set 'connection-type' + g_vpn_data["connection-type"] = "tls" + have_sk = g_switches["secret"] ~= nil + have_ca = g_vpn_data["ca"] ~= nil + have_certs = ve_ca and g_vpn_data["cert"] and g_vpn_data["key"] + if g_switches["auth-user-pass"] then + if have_certs then + g_vpn_data["connection-type"] = "password-tls" + elseif have_ca then + g_vpn_data["connection-type"] = "tls" + end + elseif have_certs then g_vpn_data["connection-type"] = "tls" + elseif have_sk then g_vpn_data["connection-type"] = "static-key" + end + return true +end + + +-------------------------------------------------------- +-- Create and write connection file in keyfile format -- +-------------------------------------------------------- +function write_vpn_to_keyfile(in_file, out_file) + connection = [[ +[connection] +id=__NAME_PLACEHOLDER__ +uuid=__UUID_PLACEHOLDER__ +type=vpn +autoconnect=no + +[ipv4] +method=auto +never-default=true +__ROUTES_PLACEHOLDER__ + +[ipv6] +method=auto + +[vpn] +service-type=org.freedesktop.NetworkManager.openvpn +]] + connection = connection .. vpn_settings_to_text(g_vpn_data) + + local routes = "" + for idx, r in ipairs(g_ip4_data["routes"] or {}) do + routes = routes .. string.format("routes%d=%s/%s,%s,%s\n", + idx, r[1], ip_mask_to_prefix(r[2]), r[3], r[4]) + end + + connection = string.gsub(connection, "__NAME_PLACEHOLDER__", (out_file:gsub(".*/", ""))) + connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid()) + connection = string.gsub(connection, "__ROUTES_PLACEHOLDER__\n", routes) + + -- write output file + local f, err = io.open(out_file, "w") + if not f then io.stderr:write(err) return false end + f:write(connection) + f:close() + + local ofname = out_file:gsub(".*/", "") + io.stderr:write("Successfully converted VPN configuration: " .. in_file .. " => " .. out_file .. "\n") + io.stderr:write("To use the connection, do:\n") + io.stderr:write("# cp " .. out_file .. " /etc/NetworkManager/system-connections\n") + io.stderr:write("# chmod 600 /etc/NetworkManager/system-connections/" .. ofname .. "\n") + io.stderr:write("# nmcli con load /etc/NetworkManager/system-connections/" .. ofname .. "\n") + return true +end + +--------------------------------------------- +-- Import VPN connection to NetworkManager -- +--------------------------------------------- +function import_vpn_to_NM(filename) + local lgi = require 'lgi' + local GLib = lgi.GLib + local NM = lgi.NM + + -- function creating NMConnection + local function create_profile(name) + local profile = NM.SimpleConnection.new() + + s_con = NM.SettingConnection.new() + s_ip4 = NM.SettingIP4Config.new() + s_vpn = NM.SettingVpn.new() + s_con[NM.SETTING_CONNECTION_ID] = name + s_con[NM.SETTING_CONNECTION_UUID] = uuid() + s_ip4[NM.SETTING_IP_CONFIG_METHOD] = NM.SETTING_IP4_CONFIG_METHOD_AUTO + s_con[NM.SETTING_CONNECTION_TYPE] = "vpn" + s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.openvpn" + + -- add routes + local AF_INET = 2 + for _, r in ipairs(g_ip4_data["routes"] or {}) do + route = NM.IPRoute.new(AF_INET, r[1], ip_mask_to_prefix(r[2]), r[3], r[4]) + s_ip4:add_route(route) + end + + -- add vpn data + for k,v in pairs(g_vpn_data) do + s_vpn:add_data_item(k, v) + end + + profile:add_setting(s_con) + profile:add_setting(s_vpn) + profile:add_setting(s_ip4) + return profile + end + + -- callback function for add_connection() + local function added_cb(client, result, data) + local con,err,code = client:add_connection_finish(result) + if con then + print(string.format("%s: Imported to NetworkManager: %s - %s", + filename, con:get_uuid(), con:get_id())) + else + io.stderr:write(code .. ": " .. err .. "\n"); + return false + end + main_loop:quit() + end + + local profile_name = string.match(filename, '[^/\\]+$') or filename + main_loop = GLib.MainLoop(nil, false) + local con = create_profile(profile_name) + local client = NM.Client.new() + + -- send the connection to NetworkManager + client:add_connection_async(con, true, nil, added_cb, nil) + + -- run main loop so that the callback could be called + main_loop:run() + return true +end + + +--------------------------- +-- Main code starts here -- +--------------------------- +local import_mode = false +local infile, outfile + +-- parse command-line arguments +if not arg[1] or arg[1] == "--help" or arg[1] == "-h" then usage() end +if arg[1] == "--import" or arg[1] == "-i" then + infile = arg[2] + if not infile then usage() end + import_mode = true +else + infile = arg[1] + outfile = arg[2] + if not infile or not outfile then usage() end + if arg[3] then usage() end +end + +if import_mode then + -- check if lgi is available + local success,msg = pcall(require, 'lgi') + if not success then + io.stderr:write("Lua lgi module is not available, please install it (usually lua-lgi package)\n") + -- print(msg) + os.exit(1) + end + -- read configs, convert them and import to NM + for i = 2, #arg do + ok, err_msg = read_and_convert(arg[i]) + if ok then import_vpn_to_NM(arg[i]) + else io.stderr:write(err_msg .. "\n") end + -- reset global vars + g_vpn_data = {} + g_ip4_data = {} + g_switches = {} + end +else + -- read configs, convert them and write as NM keyfile connection + ok, err_msg = read_and_convert(infile) + if ok then write_vpn_to_keyfile(infile, outfile) + else io.stderr:write(err_msg .. "\n") end +end + diff --git a/contrib/scripts/nm-import-vpnc b/contrib/scripts/nm-import-vpnc new file mode 100755 index 00000000..f7d5debb --- /dev/null +++ b/contrib/scripts/nm-import-vpnc @@ -0,0 +1,416 @@ +#!/usr/bin/env lua +-- SPDX-License-Identifier: GPL-2.0-or-later +-- +-- Copyright (C) 2015 Red Hat, Inc. +-- + +-- Script for importing/converting Cisco VPN configuration files (.pcf) to NetworkManager +-- In general, the implementation follows the logic of import() from +-- https://git.gnome.org/browse/network-manager-vpnc/tree/properties/nm-vpnc.c + +---------------------- +-- Helper functions -- +---------------------- +function read_all(in_file) + local f, msg = io.open(in_file, "r") + if not f then return nil, msg; end + local content = f:read("*all") + f:close() + return content +end + +function uuid() + math.randomseed(os.time()) + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + local uuid = string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end) + return uuid +end + +function vpn_settings_to_text(vpn_settings) + local t = {} + for k,v in pairs(vpn_settings) do + t[#t+1] = k.."="..v + end + return table.concat(t, "\n") +end + +function usage() + local basename = string.match(arg[0], '[^/\\]+$') or arg[0] + print(basename .. " - convert/import Cisco VPN (.pcf) configuration to NetworkManager") + print("Usage:") + print(" " .. basename .. " <input-file> <output-file>") + print(" - converts Cisco VPN config to NetworkManager keyfile") + print("") + print(" " .. basename .. " --import <input-file1> <input-file2> ...") + print(" - imports Cisco VPN config(s) to NetworkManager") + os.exit(1) +end + + +------------------------------------------- +-- Functions for VPN options translation -- +------------------------------------------- +function set_option(t, option, value) + g_switches[value[1]] = value[2] +end +function handle_generic(t, option, value) + t[option] = value[2] +end +function handle_yes(t, option, value) + t[option] = "yes" +end +function handle_bool(t, option, value) + if tonumber(value[2]) == 1 then + t[option] = "true" + elseif tonumber(value[2]) == 0 then + t[option] = "false" + else + io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) + end +end +function handle_DHGroup(t, option, value) + local dhgroups = { [1]="dh1", [2]="dh2", [5]="dh5" } + dhgroup = dhgroups[tonumber(value[2])] + if not dhgroup then io.stderr:write(string.format("Warning: invalid value for 'DHGroup': %s\n", value[2])) end + t[option] = dhgroup +end +function handle_PeerTimeout(t, option, value) + if not value[2] then io.stderr:write("Warning: ignoring invalid option 'PeerTimeout'\n") end + if tonumber(value[2]) == 0 or (tonumber(value[2]) >=10 and tonumber(value[2] <= 86400)) then + t[option] = value[2] + else io.stderr:write(string.format("Warning: invalid value for 'PeerTimeout': %s\n", value[2])) end +end +function handle_(t, option, value) + io.stderr:write("Warning: enc_GroupPwd: encrypted group passwords are not supported by this script.\n") +end +function handle_TunnelingMode(t, option, value) + if value[2] == 1 then + io.stderr:write("Warning: TCP tunneling is not supported by vpnc. " .. + "The connection will be used with TCP tunneling disabled, " .. + "however it may not work as expected.\n") + end +end +function handle_UseLegacyIKEPort(t, option, value) + if value[2] ~= 0 then + t[option] = 500 + end +end +function handle_routes(t, option, value) + local function splitroutes(str) + local sep, fields = " ", {} + local pattern = string.format("([^%s]+)", sep) + str:gsub(pattern, + function(c) + local c1,c2 = c:match("^(%d+%.%d+%.%d+%.%d+)/(%d+)$") + if c1 then + fields[#fields+1] = { c1, c2 } + else + io.stderr:write("Warning: ignoring invalid route: '" .. c .. "'\n") + end + end) + return fields + end + t[option] = splitroutes(value[2]) +end + +-- global variables - +g_vpn_data = {} +g_vpn_pwds = {} +g_con_data = {} +g_ip4_data = {} +g_switches = {} + +vpn2nm = { + ["Description"] = { nm_opt="id", func=handle_generic, tbl=g_con_data }, + ["InterfaceName"] = { nm_opt="interface-name", func=handle_generic, tbl=g_con_data }, + ["EnableLocalLAN"] = { nm_opt="never-default", func=handle_bool, tbl=g_ip4_data }, + ["X-NM-Routes"] = { nm_opt="routes", func=handle_routes, tbl=g_ip4_data }, + ["Host"] = { nm_opt="IPSec gateway", func=handle_generic, tbl=g_vpn_data }, + ["GroupName"] = { nm_opt="IPSec ID", func=handle_generic, tbl=g_vpn_data }, + ["Username"] = { nm_opt="Xauth username", func=handle_generic, tbl=g_vpn_data }, + ["UserPassword"] = { nm_opt="Xauth password", func=handle_generic, tbl=g_vpn_pwds }, + ["SaveUserPassword"] = { nm_opt="", func=set_option, tbl={} }, + ["GroupPwd"] = { nm_opt="IPSec secret", func=handle_generic, tbl=g_vpn_pwds }, + ["DHGroup"] = { nm_opt="IKE DH Group", func=handle_DHGroup, tbl=g_vpn_data }, + ["NTDomain"] = { nm_opt="Domain", func=handle_generic, tbl=g_vpn_data }, + ["SingleDES"] = { nm_opt="Enable Single DES", func=handle_yes, tbl=g_vpn_data }, + ["EnableNat"] = { nm_opt="", func=set_option, tbl={} }, + ["X-NM-Use-NAT-T"] = { nm_opt="", func=set_option, tbl={} }, + ["X-NM-Force-NAT-T"] = { nm_opt="", func=set_option, tbl={} }, + ["X-NM-SaveGroupPassword"] = { nm_opt="", func=set_option, tbl={} }, + ["UseLegacyIKEPort"] = { nm_opt="Local Port", func=handle_UseLegacyIKEPort, tbl=g_vpn_data }, + ["PeerTimeout"] = { nm_opt="DPD idle timeout (our side)", func=handle_PeerTimeout, tbl=g_vpn_data }, + ["TunnelingMode"] = { nm_opt="", func=handle_TunnelingMode, tbl= {} }, + ["enc_UserPassword"] = { nm_opt="", func=handle_enc_pwd, tbl= {} }, + ["enc_GroupPwd"] = { nm_opt="", func=handle_enc_pwd, tbl= {} }, +} + +------------------------------------------------------ +-- Read and convert the config into the global vars -- +------------------------------------------------------ +function read_and_convert(in_file) + local function line_split(str) + -- split at '=' character + local sep, fields = "=", {} + local pattern = string.format("([^%s]+)%s(.+)", sep, sep) + fields[1], fields[2] = str:match(pattern) + return fields + end + + in_text, msg = read_all(in_file) + if not in_text then return false, msg end + + -- loop through the config and convert it + for line in in_text:gmatch("[^\r\n]+") do + repeat + -- skip comments and empty lines + if line:find("^%s*[#;]") or line:find("^%s*$") then break end + -- trim leading and trailing spaces + line = line:find("^%s*$") and "" or line:match("^%s*(.*%S)") + + local words = line_split(line) + local val = vpn2nm[words[1]] + if val then + if type(val) == "table" then val.func(val.tbl, val.nm_opt, words) + else print(string.format("debug: '%s': val=%s", line, val)) end + end + until true + end + + -- check if mandatory options exist + if not g_vpn_data["IPSec gateway"] then + local msg = in_file .. ": Not a valid Cisco VPN configuration (no Host)" + return false, msg + end + if not g_vpn_data["IPSec ID"] then + local msg = in_file .. ": Not a valid OpenVPN configuration (no GroupName)" + return false, msg + end + + -- process inter-option dependencies + -- NAT traversal mode + local natt_mode = { + NONE = "none", + NATT = "natt", + NATT_ALWAYS = "force-natt", + CISCO = "cisco-udp" + } + g_vpn_data["NAT Traversal Mode"] = natt_mode.CISCO + if tonumber(g_switches["EnableNat"]) == 0 then + g_vpn_data["NAT Traversal Mode"] = natt_mode.NONE + elseif tonumber(g_switches["EnableNat"]) == 1 then + if tonumber(g_switches["X-NM-Force-NAT-T"]) == 1 then + g_vpn_data["NAT Traversal Mode"] = natt_mode.NATT_ALWAYS + elseif tonumber(g_switches["X-NM-Use-NAT-T"]) == 1 then + g_vpn_data["NAT Traversal Mode"] = natt_mode.NATT + end + else + io.stderr:write("Warning: invalid value for EnableNat\n") + g_vpn_data["NAT Traversal Mode"] = natt_mode.CISCO + end + + -- set secret flags + g_vpn_data["Xauth password-flags"] = 1 + if tonumber(g_switches["SaveUserPassword"]) == 1 then + g_vpn_data["xauth-password-type"] = "save" + else + g_vpn_data["Xauth password-flags"] = 3 + end + if g_vpn_data["IPSec ID"] then + g_vpn_data["IPSec ID-flags"] = 1 + end + if g_switches["X-NM-SaveGroupPassword"] then + if tonumber(g_switches["X-NM-SaveGroupPassword"]) == 1 then + g_vpn_data["ipsec-secret-type"] = "save" + g_vpn_data["IPSec ID-flags"] = 1 + else + g_vpn_data["IPSec ID-flags"] = 3 + end + else + g_vpn_data["ipsec-secret-type"] = "save" + end + + return true +end + + +-------------------------------------------------------- +-- Create and write connection file in keyfile format -- +-------------------------------------------------------- +function write_vpn_to_keyfile(in_file, out_file) + connection = [[ +[connection] +id=__NAME_PLACEHOLDER__ +uuid=__UUID_PLACEHOLDER__ +__IFNAME_PLACEHOLDER__ +type=vpn +autoconnect=no + +[ipv4] +method=auto +never-default=__NEVER_DEFAULT_PLACEHOLDER__ +__ROUTES_PLACEHOLDER__ + +[ipv6] +method=auto + +[vpn] +service-type=org.freedesktop.NetworkManager.vpnc +]] + connection = connection .. vpn_settings_to_text(g_vpn_data) + connection = connection .. "\n\n[vpn-secrets]\n" + connection = connection .. vpn_settings_to_text(g_vpn_pwds) + + local con_name = g_con_data["id"] or (out_file:gsub(".*/", "")) + local ifname = g_con_data["interface-name"] + local never_default = g_ip4_data["never-default"] or "false" + local routes = "" + if ifname then ifname = "interface-name="..ifname.."\n" else ifname = "" end + for idx, r in ipairs(g_ip4_data["routes"] or {}) do + routes = routes .. string.format("routes%d=%s/%s\n", idx, r[1], r[2]) + end + + connection = string.gsub(connection, "__NAME_PLACEHOLDER__", con_name) + connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid()) + connection = string.gsub(connection, "__IFNAME_PLACEHOLDER__\n", ifname) + connection = string.gsub(connection, "__NEVER_DEFAULT_PLACEHOLDER__", never_default) + connection = string.gsub(connection, "__ROUTES_PLACEHOLDER__\n", routes) + + -- write output file + local f, err = io.open(out_file, "w") + if not f then io.stderr:write(err) return false end + f:write(connection) + f:close() + + local ofname = out_file:gsub(".*/", "") + io.stderr:write("Successfully converted VPN configuration: " .. in_file .. " => " .. out_file .. "\n") + io.stderr:write("To use the connection, do:\n") + io.stderr:write("# cp " .. out_file .. " /etc/NetworkManager/system-connections\n") + io.stderr:write("# chmod 600 /etc/NetworkManager/system-connections/" .. ofname .. "\n") + io.stderr:write("# nmcli con load /etc/NetworkManager/system-connections/" .. ofname .. "\n") + return true +end + +--------------------------------------------- +-- Import VPN connection to NetworkManager -- +--------------------------------------------- +function import_vpn_to_NM(filename) + local lgi = require 'lgi' + local GLib = lgi.GLib + local NM = lgi.NM + + -- function creating NMConnection + local function create_profile(name) + local profile = NM.SimpleConnection.new() + local never_default = g_ip4_data["never-default"] == "true" + + s_con = NM.SettingConnection.new() + s_vpn = NM.SettingVpn.new() + s_ip4 = NM.SettingIP4Config.new() + + s_con[NM.SETTING_CONNECTION_ID] = name + s_con[NM.SETTING_CONNECTION_UUID] = uuid() + s_con[NM.SETTING_CONNECTION_INTERFACE_NAME] = g_con_data["interface-name"] + s_con[NM.SETTING_CONNECTION_TYPE] = "vpn" + s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.vpnc" + s_ip4[NM.SETTING_IP_CONFIG_METHOD] = NM.SETTING_IP4_CONFIG_METHOD_AUTO + s_ip4[NM.SETTING_IP_CONFIG_NEVER_DEFAULT] = never_default + + -- add routes + local AF_INET = 2 + for _, r in ipairs(g_ip4_data["routes"] or {}) do + route = NM.IPRoute.new(AF_INET, r[1], r[2], nil, -1) + s_ip4:add_route(route) + end + + -- add vpn data + for k,v in pairs(g_vpn_data) do + s_vpn:add_data_item(k, v) + end + -- add vpn secrets + for k,v in pairs(g_vpn_pwds) do + s_vpn:add_secret(k, v) + end + + profile:add_setting(s_con) + profile:add_setting(s_vpn) + profile:add_setting(s_ip4) + return profile + end + + -- callback function for add_connection() + local function added_cb(client, result, data) + local con,err,code = client:add_connection_finish(result) + if con then + print(string.format("%s: Imported to NetworkManager: %s - %s", + filename, con:get_uuid(), con:get_id())) + else + io.stderr:write(code .. ": " .. err .. "\n"); + return false + end + main_loop:quit() + end + + local profile_name = g_con_data["id"] or string.match(filename, '[^/\\]+$') or filename + main_loop = GLib.MainLoop(nil, false) + local con = create_profile(profile_name) + local client = NM.Client.new() + + -- send the connection to NetworkManager + client:add_connection_async(con, true, nil, added_cb, nil) + + -- run main loop so that the callback could be called + main_loop:run() + return true +end + + +--------------------------- +-- Main code starts here -- +--------------------------- +local import_mode = false +local infile, outfile + +-- parse command-line arguments +if not arg[1] or arg[1] == "--help" or arg[1] == "-h" then usage() end +if arg[1] == "--import" or arg[1] == "-i" then + infile = arg[2] + if not infile then usage() end + import_mode = true +else + infile = arg[1] + outfile = arg[2] + if not infile or not outfile then usage() end + if arg[3] then usage() end +end + +if import_mode then + -- check if lgi is available + local success,msg = pcall(require, 'lgi') + if not success then + io.stderr:write("Lua lgi module is not available, please install it (usually lua-lgi package)\n") + -- print(msg) + os.exit(1) + end + -- read configs, convert them and import to NM + for i = 2, #arg do + ok, err_msg = read_and_convert(arg[i]) + if ok then import_vpn_to_NM(arg[i]) + else io.stderr:write(err_msg .. "\n") end + -- reset global vars + g_vpn_data = {} + g_vpn_pwds = {} + g_con_data = {} + g_ip4_data = {} + g_switches = {} + end +else + -- read configs, convert them and write as NM keyfile connection + ok, err_msg = read_and_convert(infile) + if ok then write_vpn_to_keyfile(infile, outfile) + else io.stderr:write(err_msg .. "\n") end +end + diff --git a/contrib/scripts/nm-python-black-format.sh b/contrib/scripts/nm-python-black-format.sh new file mode 100755 index 00000000..eae84fdb --- /dev/null +++ b/contrib/scripts/nm-python-black-format.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +set -e + +_print() { + printf '%s\n' "$*" >&2 +} + +die() { + _print "$*" + exit 1 +} + +NM_ROOT="$(git rev-parse --show-toplevel)" || die "not inside a git repository" +NM_PREFIX="$(git rev-parse --show-prefix)" || die "not inside a git repository" + +cd "$NM_ROOT" || die "failed to cd into \$NM_ROOT\"" + +if [ ! -f "./src/core/main.c" ]; then + die "Error: \"$NM_ROOT\" does not look like NetworkManager source tree" +fi + +BLACK="${BLACK:-black}" + +if ! command -v "$BLACK" &> /dev/null; then + _print "Error: black is not installed. On RHEL/Fedora/CentOS run 'dnf install black'" + exit 77 +fi + +OLD_IFS="$IFS" + +usage() { + printf "Usage: %s [OPTION]...\n" "$(basename "$0")" + printf "Reformat python source files using python black.\n\n" + printf "OPTIONS:\n" + printf " -i Reformat files (this is the default)\n" + printf " -n|--dry-run|--check Only check the files (contrary to \"-i\")\n" + printf " --show-filenames Only print the filenames that would be checked/formatted\n" + printf " -h Print this help message\n" +} + +TEST_ONLY=0 +SHOW_FILENAMES=0 + +while (( $# )); do + case "$1" in + -h) + usage + exit 0 + ;; + -n|--dry-run|--check) + TEST_ONLY=1 + shift + continue + ;; + -i) + TEST_ONLY=0 + shift + continue + ;; + --show-filenames) + SHOW_FILENAMES=1 + shift + continue + ;; + *) + usage + exit 1 + ;; + esac +done + +IFS=$'\n' +FILES=() +FILES+=( $(git ls-tree --name-only -r HEAD | grep '\.py$') ) +FILES+=( $(git grep -l '#!.*\<p[y]thon3\?\>') ) +FILES=( $(printf "%s\n" "${FILES[@]}" | sort -u) ) + +# Filter out paths that are forked from upstream projects and not +# ours to reformat. +FILES=( $( + printf "%s\n" "${FILES[@]}" | + sed \ + -e '/^src\/[cn]-[^/]\+\//d' \ + -e '/^src\/libnm-systemd-[^/]\+\/src\//d' +) ) + +IFS="$OLD_IFS" + +if [ $SHOW_FILENAMES = 1 ]; then + printf '%s\n' "${FILES[@]}" + exit 0 +fi + +EXTRA_ARGS=() +if [ $TEST_ONLY = 1 ]; then + EXTRA_ARGS+=('--check') +fi + +"$BLACK" "${EXTRA_ARGS[@]}" "${FILES[@]}" diff --git a/contrib/scripts/nm-setup-git.sh b/contrib/scripts/nm-setup-git.sh new file mode 100755 index 00000000..32e059ad --- /dev/null +++ b/contrib/scripts/nm-setup-git.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +set -e + +usage() { + printf "%s [--no-test]\n" "$CMD_NAME" + printf "\n" + printf "This script configures (or shows configuration) to the local git, with\n" + printf "settings that might be useful when working on NetworkManager.\n" + printf "\n" + printf "RUn it without arguments, it only prints and shows what it would do.\n" + printf "\n" + printf " --no-test: by default, the script only prints what it\n" + printf " would do. You can also set NO_TEST=1 environment variable.\n" + printf "\n" +} + +get_bool() { + local name="$1" + local val="${!name}" + + case "$val" in + 1|y|yes|Yes|YES|true|True|TRUE|on|On|ON) + echo -n 1 + return 0 + ;; + 0|n|no|No|NO|false|False|FALSE|off|Off|OFF) + echo -n 0 + return 0 + ;; + *) + printf "%s" "$2" + ;; + esac +} + +die() { + echo "ERROR: $*" + exit 1 +} + +_pprint() { + local a + local sp='' + + for a; do + printf "$sp%q" "$a" + sp=' ' + done +} + +call() { + local m="" + + [ "$SKIP" = 1 ] && m="SKIP: " + + if [ "$NO_TEST" != 1 ]; then + printf "WOULD: %s%s\n" "$m" "$(_pprint "$@")" + return 0 + fi + printf "CALL: %s%s\n" "$m" "$(_pprint "$@")" + [ "$SKIP" = 1 ] || "$@" +} + +git_config_reset() { + local key="$1" + local val="$2" + local c=(git config --replace-all "$key" "$val") + + test "$#" -eq 2 || die "invalid arguments to git_config_add(): $@" + + if [ "$(git config --get-all "$key")" = "$val" ]; then + SKIP=1 call "${c[@]}" + return 0 + fi + call "${c[@]}" +} + +git_config_add() { + local key="$1" + local val="$2" + local c=(git config --add "$key" "$val") + + test "$#" -eq 2 || die "invalid arguments to git_config_add(): $@" + + if git config --get-all "$key" | grep -qFx "$val"; then + SKIP=1 call "${c[@]}" + return 0 + fi + call "${c[@]}" +} + +CMD_NAME="$0" +NO_TEST="$(get_bool NO_TEST 0)" + +for a; do + case "$a" in + --no-test) + NO_TEST=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + usage + die "Invalid argument \"$a\"" + ;; + esac +done + +case "$(git config --get-all remote.origin.url)" in + "https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git"| \ + "git@gitlab.freedesktop.org:NetworkManager/NetworkManager.git"| \ + "ssh://git@gitlab.freedesktop.org/NetworkManager/NetworkManager") + ;; + *) + die "unexpected git repository. Expected that remote.origin.url is set to \"https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git\"" + ;; +esac + +git_config_add blame.ignoreRevsFile '.git-blame-ignore-revs' +git_config_reset blame.markIgnoredLines true +git_config_reset blame.markUnblamableLines true +git_config_add notes.displayref 'refs/notes/bugs' +git_config_add remote.origin.fetch 'refs/notes/bugs:refs/notes/bugs' +git_config_reset remote.origin.pushurl 'git@gitlab.freedesktop.org:NetworkManager/NetworkManager.git' +git_config_add 'alias.backport-merge' '! (git show main:contrib/scripts/git-backport-merge || git show origin/main:contrib/scripts/git-backport-merge) | bash -s -' + +if [ "$NO_TEST" != 1 ]; then + printf "Run with \"--no-test\" or see \"-h\"\n" >&2 + printf "\n" >&2 + printf " \"%s\" --no-test\n" "$CMD_NAME" >&2 +fi diff --git a/contrib/scripts/test-create-many-device-setup.sh b/contrib/scripts/test-create-many-device-setup.sh new file mode 100755 index 00000000..55f2a1c6 --- /dev/null +++ b/contrib/scripts/test-create-many-device-setup.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +set -x + +die() { + printf '%s\n' "$*" >&1 + exit 1 +} + +ARG_OP="$1" +shift +test -n "$ARG_OP" || die "specify the operation (setup, cleanup)" + +test "$USER" = root || die "must run as root" + +NUM_DEVS="${NUM_DEVS:-50}" +NUM_VLAN_DEVS="${NUM_VLAN_DEVS:-0}" + + +DNSMASQ_PIDFILE="/tmp/nm-test-create-many-device-setup.dnsmasq.pid" +NM_TEST_CONF="/etc/NetworkManager/conf.d/99-my-test.conf" +TEST_NETNS="T" + + +_do_service() { + test "$DO_SERVICE" = 1 || return 0 + "$@" +} + +_dnsmasq_kill() { + pkill -F "$DNSMASQ_PIDFILE" + rm -rf "$DNSMASQ_PIDFILE" +} + +_link_delete_all() { + ip link | sed -n 's/^[0-9]\+:.*\(t-[^@:]\+\)@.*/\1/p' | xargs -n 1 ip link delete +} + +cleanup_base() { + ip netns delete "$TEST_NETNS" + _dnsmasq_kill + _link_delete_all + rm -rf "$NM_TEST_CONF" + rm -rf /run/NetworkManager/system-connections/c-*.nmconnection +} + +cmd_cleanup() { + _do_service systemctl stop NetworkManager + cleanup_base + systemctl unmask NetworkManager-dispatcher + systemctl enable NetworkManager-dispatcher + _do_service systemctl start NetworkManager +} + +cmd_setup() { + + _do_service systemctl stop NetworkManager + systemctl mask NetworkManager-dispatcher + systemctl stop NetworkManager-dispatcher + + cleanup_base + + ip netns add "$TEST_NETNS" + ip --netns "$TEST_NETNS" link add t-br0 type bridge + ip --netns "$TEST_NETNS" link set t-br0 type bridge stp_state 0 + ip --netns "$TEST_NETNS" link set t-br0 up + ip --netns "$TEST_NETNS" addr add 172.16.0.1/16 dev t-br0 + ip netns exec "$TEST_NETNS" \ + dnsmasq \ + --conf-file=/dev/null \ + --pid-file="$DNSMASQ_PIDFILE" \ + --no-hosts \ + --keep-in-foreground \ + --bind-interfaces \ + --except-interface=lo \ + --clear-on-reload \ + --listen-address=172.16.0.1 \ + --dhcp-range=172.16.1.1,172.16.20.1,60 \ + --no-ping \ + & + disown + for i in `seq "$NUM_DEVS"`; do + ip --netns "$TEST_NETNS" link add t-a$i type veth peer t-b$i + ip --netns "$TEST_NETNS" link set t-a$i up + ip --netns "$TEST_NETNS" link set t-b$i up master t-br0 + done + for i in `seq "$NUM_VLAN_DEVS"`; do + ip --netns "$TEST_NETNS" link add link t-b1 name t-b1.$i type vlan id $i + ip --netns "$TEST_NETNS" link set t-b1.$i up master t-br0 + done + + cat <<EOF > "$NM_TEST_CONF" +[main] +dhcp=internal +no-auto-default=interface-name:t-a* +[device-99-my-test] +match-device=interface-name:t-a* +managed=1 +[logging] +level=INFO +[connectivity] +enabled=0 +EOF + + _do_service systemctl start NetworkManager + + for i in `seq "$NUM_DEVS"`; do + ip --netns "$TEST_NETNS" link set t-a$i netns $$ + done + + if [ "$DO_ADD_CON" = 1 ]; then + for i in `seq "$NUM_DEVS"`; do + nmcli connection add save no type ethernet con-name c-a$i ifname t-a$i autoconnect no ipv4.method auto ipv6.method auto + done + fi + + if [ "$DO_ADD_VLAN_CON" = 1 ]; then + for i in `seq "$NUM_VLAN_DEVS"`; do + nmcli connection add save no type bridge con-name c-a1.$i-br ifname t-a1.$i.br autoconnect no ipv4.method auto ipv6.method auto bridge.stp 0 + nmcli connection add save no type vlan con-name c-a1.$i-po ifname t-a1.$i.po autoconnect no vlan.id $i vlan.parent t-a1 master c-a1.$i-br slave-type bridge + done + fi +} + + +case "$ARG_OP" in + "setup") + cmd_setup + ;; + "cleanup") + cmd_cleanup + ;; + *) + die "Unknown command \"$ARG_OP\"" + ;; +esac diff --git a/contrib/scripts/test-macsec b/contrib/scripts/test-macsec new file mode 100755 index 00000000..93935865 --- /dev/null +++ b/contrib/scripts/test-macsec @@ -0,0 +1,102 @@ +#!/bin/sh + +# Test for MACsec in PSK mode + +if [ "$#" = 2 ]; then + # DHCP helper + dev=$1 + addr=$2 + net=${addr%.*} + + while [ ! -d "/sys/class/net/$dev" ]; do + sleep 1 + done + + ip a add $addr/24 dev "$dev" + + dnsmasq --conf-file --no-hosts --keep-in-foreground --listen-address=$addr \ + --dhcp-range=$net.250,$net.255,60m -i "$dev" \ + --bind-interface --except-interface=lo + + exit 0 +fi + +TMPDIR=$(mktemp -d /tmp/macsec-XXXXXX) +ADDR=172.16.10.1 +MKA_CAK=00112233445566778899001122334455 +MKA_CKN=5544332211009988776655443322110055443322110099887766554433221100 + +trap 'rm -rf "$TMPDIR"; kill $(jobs -p)' EXIT + +echo "* Setup..." + +# Clean up +ip netns del macsec-ns 2> /dev/null +ip link del macsec-veth 2> /dev/null +# Create namespace +ip netns add macsec-ns +# Create interfaces +ip link add macsec-veth type veth peer name macsec-vethp +# Move interfaces into namespace +ip link set macsec-vethp netns macsec-ns +# Bring up interfaces +ip link set macsec-veth up +ip -n macsec-ns link set macsec-vethp up + +echo "* Start wpa_supplicant..." + +cat <<EOF > $TMPDIR/wpa_supplicant.conf +ctrl_interface=/run/hostapd1 +eapol_version=3 +ap_scan=0 +fast_reauth=1 +network={ + key_mgmt=NONE + eapol_flags=0 + macsec_policy=1 + mka_cak=$MKA_CAK + mka_ckn=$MKA_CKN +} +EOF +ip netns exec macsec-ns wpa_supplicant \ + -c "$TMPDIR/wpa_supplicant.conf" -i macsec-vethp -Dmacsec_linux -dd > /dev/null 2>&1 & +ip netns exec macsec-ns $0 macsec0 $ADDR > /dev/null 2>&1 & + +echo "* Create connections..." + +nmcli connection delete test-macsec+ test-veth+ > /dev/null 2>&1 +nmcli connection add type ethernet ifname macsec-veth con-name test-veth+ \ + ipv4.method disabled ipv6.method ignore +nmcli connection add type macsec con-name test-macsec+ ifname macsec0 \ + connection.autoconnect no \ + macsec.parent macsec-veth macsec.mode psk \ + macsec.mka-cak $MKA_CAK \ + macsec.mka-cak-flags 0 \ + macsec.mka-ckn $MKA_CKN + +echo "* Bring up connections..." +nmcli connection up test-veth+ +nmcli connection up test-macsec+ + +echo "* Test connectivity..." +ping $ADDR -c2 -q > /dev/null +res=$? + +echo "* Clean up..." + +nmcli connection delete test-macsec+ test-veth+ > /dev/null 2>&1 +ip link del macsec-veth 2> /dev/null +ip netns del macsec-ns 2> /dev/null + +echo + +if [ "$res" = 0 ]; then + echo "Success" +else + echo "Failure" +fi + +exit $res + + + diff --git a/contrib/scripts/test-ppp.sh b/contrib/scripts/test-ppp.sh new file mode 100755 index 00000000..c100e976 --- /dev/null +++ b/contrib/scripts/test-ppp.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# test-ppp.sh: +# +# Test script that creates an netns and connect it with +# veth pairs. On the other end, it runs pppoe-server. +# It also creates a NetworkManager profile that can be activated. +# +# Usage: +# +# ./test-ppp.sh [setup]: create the setup. This implies a "cleanup" +# first. +# ./test-ppp.sh cleanup: cleanup the things that the script created. +set -e + +export IFACE=net1 +export IFACE_PEER=net1-x +export CON_NAME="ppp-$IFACE" +export NETNS=nm-ppp +export PPP_SERVICE=isp +export PPP_AUTH=pap +export PPP_USER=test-user +export PPP_PASSWD=test-passwd +export IP_PEER="192.168.133.6" +export IP_RANGE="192.168.133.100-130" + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +do_cleanup() { + pkill -F "/tmp/nm-test-ppp-$IFACE.pid" pppoe-server &>/dev/null || : + rm -rf \ + "/tmp/nm-test-ppp-$IFACE.pid" \ + "/tmp/nm-test-ppp-allip-$IFACE" \ + "/tmp/nm-test-ppp-pppoe-server-options-$IFACE" \ + "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets" + ip --netns "$NETNS" link delete "$IFACE_PEER" &>/dev/null || : + ip netns delete "$NETNS" &>/dev/null || : + + nmcli connection delete id ppp-net1 || : +} + +do_setup() { + do_cleanup + + ip netns add "$NETNS" + ip --netns "$NETNS" link add "$IFACE" type veth peer "$IFACE_PEER" + ip --netns "$NETNS" link set "$IFACE_PEER" up + + ip --netns "$NETNS" addr add "$IP_PEER/24" dev "$IFACE_PEER" + + echo "$IP_RANGE" > "/tmp/nm-test-ppp-allip-$IFACE" + + cat <<EOF > "/tmp/nm-test-ppp-pppoe-server-options-$IFACE" +require-$PPP_AUTH +lcp-echo-interval 10 +lcp-echo-failure 2 +ms-dns 8.8.8.8 +ms-dns 8.8.4.4 +netmask 255.255.255.0 +defaultroute +noipdefault +usepeerdns +EOF + + echo "$PPP_USER * $PPP_PASSWD $IP_PEER" > "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets" + chmod 600 "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets" + mkdir -p /etc/ppp + touch "/etc/ppp/$PPP_AUTH-secrets" + ip netns exec "$NETNS" bash -ex <( + cat <<'EOF' + mount -o bind "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets" "/etc/ppp/$PPP_AUTH-secrets" && + exec pppoe-server \ + -X "/tmp/nm-test-ppp-$IFACE.pid" \ + -S "$PPP_SERVICE" \ + -C "$PPP_SERVICE" \ + -L "$IP_PEER" \ + -p "/tmp/nm-test-ppp-allip-$IFACE" \ + -I "$IFACE_PEER" \ + -O "/tmp/nm-test-ppp-pppoe-server-options-$IFACE" +EOF +) & + + ip --netns "$NETNS" link set "$IFACE" netns $$ + + nmcli connection add \ + type pppoe \ + con-name "$CON_NAME" \ + ifname "ppp-$IFACE" \ + pppoe.parent "$IFACE" \ + service "$PPP_SERVICE" \ + username "$PPP_USER" \ + password "$PPP_PASSWD" \ + autoconnect no +} + +CMD="${1-setup}" +case "$CMD" in + setup| \ + cleanup) + "do_$CMD" + ;; + *) + die "invalid command $1" + ;; +esac diff --git a/contrib/scripts/test-prefix-delegation.sh b/contrib/scripts/test-prefix-delegation.sh new file mode 100755 index 00000000..7fc4140e --- /dev/null +++ b/contrib/scripts/test-prefix-delegation.sh @@ -0,0 +1,151 @@ +#!/bin/sh + +# Usage: ./test-prefix-delegation {ll|slaac|dhcp-stateful|dhcp-stateless} + +MODE=${1:-dhcp-stateful} + +cleanup() +{ + pkill -F dhcpd.pid + pkill -F radvd.pid + rm -f radvd.conf + rm -f dhcpd.conf + rm -f leases.conf + nmcli connection delete v1+ v2+ + ip netns del ns1 + ip netns del ns2 + ip link del v1 + ip link del v2 +} + +require() +{ + if ! command -v "$1" > /dev/null ; then + echo " *** Error: command '$1' not found" + exit 1 + fi +} + +exit_hook() +{ + cleanup > /dev/null 2>&1 +} + +require nmcli +require ip +require jq +require radvd +require dhcpd + +unalias ip 2> /dev/null + +cleanup +trap exit_hook EXIT + +# ns1 is the 'upstream' namespace that provides IPv6 connectivity +# through RA and DHCPv6. The DHCP server also acts as a delegating +# router for /60 prefixes. + +# ns2 is the 'downstream' namespace where a client obtains IPv6 +# connectivity through RA from NM. + +# NM is in the default namespace and has a connection to ns1 with +# ipv6.method=auto and to ns2 with ipv6.method=shared. + +ip netns add ns1 +ip netns add ns2 + +ip link add v1 type veth peer name v1p +ip link add v2 type veth peer name v2p + +ip link set v1p netns ns1 +ip link set v2p netns ns2 + +ip link set v1 up +ip link set v2 up + +ip -n ns1 link set v1p up +ip -n ns1 addr add dev v1p fc01::1/64 + +ip -n ns2 link set v2p up + +if [ "$MODE" = ll ]; then + adv_managed=off + adv_other=off +elif [ "$MODE" = slaac ]; then + adv_managed=off + adv_other=off + adv_prefix="prefix fc01::/64 {AdvOnLink on; AdvAutonomous on; AdvRouterAddr off; };" +elif [ "$MODE" = dhcp-stateless ]; then + adv_managed=off + adv_other=on + adv_prefix="prefix fc01::/64 {AdvOnLink on; AdvAutonomous on; AdvRouterAddr off; };" +elif [ "$MODE" = dhcp-stateful ]; then + adv_managed=on + adv_other=off + dhcp_range="range6 fc01::1000 fc01::ffff;" +else + echo "Unknown mode '$MODE'" + exit 1 +fi + +echo "Starting in $MODE mode..." + +cat > radvd.conf <<EOF +interface v1p { + AdvSendAdvert on; + AdvManagedFlag ${adv_managed}; + AdvOtherConfigFlag ${adv_other}; + MinRtrAdvInterval 3; + MaxRtrAdvInterval 60; + ${adv_prefix} +}; +EOF + +cat > dhcpd.conf <<EOF +subnet6 fc01::/64 { + ${dhcp_range} + prefix6 fc01:bbbb:1:: fc01:bbbb:2:: / 60; + option dhcp6.name-servers fc01::8888; +} +EOF + +echo > leases.conf +ip netns exec ns1 radvd -n -C radvd.conf -p radvd.pid & +ip netns exec ns1 dhcpd -6 -d -cf dhcpd.conf -lf leases.conf -pf dhcpd.pid & + +nmcli connection add type ethernet ifname v1 con-name v1+ ipv4.method disabled ipv6.method auto autoconnect no +nmcli connection add type ethernet ifname v2 con-name v2+ ipv4.method disabled ipv6.method shared autoconnect no + +nmcli connection up v1+ + +sleep 5 + +nmcli connection up v2+ + +sleep 5 + +ip a show dev v1 +ip a show dev v2 + +addr=$(ip -j addr show dev v1 | jq -r '.[0].addr_info[] | select(.scope=="link")'.local) +prefix="fc01:bbbb:1::/32" +ip netns exec ns1 ip route add $prefix via $addr dev v1p + +# kernel does IPv6 autoconf in ns2 ... + +sleep 10 + +# check connectivity to ns1 +if ! ip -n ns2 a show dev v2p | grep 'fc01:bbbb:[a-f0-9\:]\+/64'; then + ip -n ns2 a show dev v2p + echo "ERROR: no address" + exit 1 +fi + +if ! ip netns exec ns2 ping -c2 fc01::1; then + echo "ERROR: ping failed" + exit 1 +fi + +echo "OK" |