chiark / gitweb /
ipif: service-wrap: Scan directories
[userv-utils] / ipif / service-wrap
CommitLineData
08e5c1c8
IJ
1#!/usr/bin/perl -w
2#
3# When invoked appropriately, it creates a point-to-point network
4# interface with specified parameters. It arranges for packets sent out
5# via that interface by the kernel to appear on its own stdout in SLIP or
6# CSLIP encoding, and packets injected into its own stdin to be given to
7# the kernel as if received on that interface. Optionally, additional
8# routes can be set up to arrange for traffic for other address ranges to
9# be routed through the new interface.
10#
11# This is the access control wrapper for the service program.
12# Arrangments should be made to invoke this as root from userv.
13#
14# Usage:
15#
44b7fe58 16# .../ipif1 <v1config> <real-service-program> -- <service-args>...
08e5c1c8 17#
4e5552a4
IJ
18# Config file is a series of lines, or a directory. If a directory,
19# all files with names matching ^[-A-Za-z0-9_]+$ are processed.
08e5c1c8
IJ
20#
21# permit <keyword>....
22#
23# if caller, local addr, all remote addrs and networks, and
24# ifname, all match, permits the request (and stops reading
25# the config)
26#
27# group <groupname>|<gid>
28# matches caller if they are in that group
29# user <username>|<uid>
30# matches caller if they are that user
31# everyone
32# always matches caller
33#
34# hostnet <ipaddr>/<prefixlen>
35# equivalent to local <ipv4addr> remote <ipv4addr&prefix>
36# local <ipaddr>
37# matches local address when it is <ipv4addr>
38# remote <ipnetnet>/<prefixlen>
39# matches aplicable remote addrs (including p-t-p)
40# addrs <ipaddr>|<ipnetnet>/<prefixlen>
41# matches applicable local ore remote addrs
42#
43# ifname <ifname>
44# matches interface name if it is exactly <ifname>
45# (<ifname> may contain %d, which is interpreted by
46# the kernel)
47# wildcards are not supported
48# if a permit has no ifname at all, it is as if
49# `ifname userv%d' was specified
50#
4e5552a4 51# include <other-config-file-or-directory>
44b7fe58 52#
08e5c1c8
IJ
53# v0config <v0configfile>
54#
55# If none of the `permit' lines match, will read <v0configfile>
44b7fe58 56# in old format. Must come after all `permit' lines.
08e5c1c8
IJ
57#
58# <config> --
59
60use strict;
8ca56de8
IJ
61use POSIX;
62use Carp;
44b7fe58 63use NetAddr::IP::Lite qw(:nofqdn :lower);
44b7fe58
IJ
64
65our $default_ifname = 'userv%d';
66
8ca56de8
IJ
67sub badusage ($) {
68 my ($m) = @_;
69 die "bad usage: $m\n";
70}
71
44b7fe58
IJ
72sub oneaddr ($) {
73 my ($ar) = @_;
8ca56de8 74 my $x = $$ar;
44b7fe58
IJ
75 $x // badusage "missing IP address";
76 $x = new NetAddr::IP::Lite $x // badusage "bad IP address";
77 $x->masklen == $x->bits or badusage "IP network where addr expected";
78 die if $x->addr =~ m,/,;
79 $$ar = $x;
80}
81
82@ARGV == 5 or badusage "wrong number of arguments";
83our ($v1config, $realservice, $sep, $addrsarg, $rnets) = @ARGV;
84
85$sep eq '--' or badusage "separator should be \`--'";
86my ($local_addr, $peer_addr, $mtu, $protocol, $ifname) =
87 split /\,/, $addrsarg;
88
89oneaddr \$local_addr;
90oneaddr \$peer_addr;
91$mtu = 1500 unless length $mtu;
92$mtu =~ m/^[1-9]\d{1,4}/ or badusage "bad mtu";
93$mtu += 0;
94
95$protocol = 'slip' unless length $protocol;
96$protocol =~ m/\W/ and badusage "bad protocol";
97
98$ifname = $default_ifname unless length $ifname;
99
100our @rnets = ($rnets eq '-' ? () : split /\,/, $rnets);
101@rnets = map { new NetAddr::IP::Lite $_ } @rnets;
102
8ca56de8
IJ
103
104sub execreal ($) {
105 my ($use_v0config) = @_;
106 exec $realservice, $use_v0config, '--',
0d8db366
IJ
107 (join ',', $local_addr->addr, $peer_addr->addr,
108 $mtu, $protocol, $ifname),
8ca56de8
IJ
109 @rnets ? (join ",", map { "$_" } @rnets) : "-"
110 or die "exec $realservice: $!\n";
111}
112
113our $v0config;
114
115our $cfgpath;
116
117sub badcfg ($) {
118 my ($m) = @_;
119 die "bad configuration: $cfgpath:$.: $m\n";
120}
121
44b7fe58
IJ
122our %need_allow;
123# $need_allow{CLASS}[]
124# $need_allow{CLASS}[]{Desc} # For error messages
125# $need_allow{CLASS}[]{Allow} # Starts out nonexistent
126# $need_allow{CLASS}[]{IpAddr} # CLASS eq Local or Remote only
127
8ca56de8 128sub allowent ($@) {
44b7fe58
IJ
129 my ($desc, @xtra) = @_;
130 return { Desc => $desc, @xtra };
131}
8ca56de8
IJ
132sub allowent_addr ($$) {
133 my ($what, $addr) = @_;
134 return allowent "$what $addr", IpAddr => $addr;
135}
136sub need_allow_item ($$) {
137 my ($cl, $ne) = @_;
138 push @{ $need_allow{$cl} }, $ne
44b7fe58
IJ
139}
140sub need_allow_singleton ($$) {
8ca56de8
IJ
141 my ($cl, $ne) = @_;
142 $need_allow{$cl} ||= [ $ne ];
44b7fe58
IJ
143}
144
145sub maybe_allow__entry ($$) {
146 my ($ne, $yes) = @_;
147 $ne->{Allowed} ||= $yes;
148}
8ca56de8 149sub maybe_allow_singleton ($$) {
44b7fe58
IJ
150 my ($cl, $yes) = @_;
151 my $ents = $need_allow{$cl};
152 die $cl unless @$ents==1;
8ca56de8 153 maybe_allow__entry $ents->[0], $yes;
44b7fe58
IJ
154}
155sub default_allow_singleton ($$) {
156 # does nothing if maybe_allow_singleton was called for this $cl;
157 # otherwise allows the singleton iff $yes
158 my ($cl, $yes) = @_;
159 my $ents = $need_allow{$cl};
160 die $cl unless @$ents==1;
161 $ents->[0]{Allowed} //= $yes;
162}
8ca56de8 163sub maybe_allow_caller_env ($$$) {
44b7fe58
IJ
164 my ($spec, @envvars) = @_;
165 foreach my $envvar (@envvars) {
166 my $val = $ENV{$envvar} // die $envvar;
167 my @vals = split / /, $val;
8ca56de8 168 #use Data::Dumper; print Dumper($spec,$envvar,\@vals);
44b7fe58
IJ
169 maybe_allow_singleton 'Caller', !!grep { $_ eq $spec } @vals;
170 }
171}
8ca56de8 172sub maybe_allow_addrs ($$) {
44b7fe58
IJ
173 my ($cl, $permitrange) = @_;
174 foreach my $ne (@{ $need_allow{$cl} }) {
8ca56de8
IJ
175 confess unless defined $ne->{IpAddr};
176 maybe_allow__entry $ne, $permitrange->contains($ne->{IpAddr});
44b7fe58
IJ
177 }
178}
179
4e5552a4 180sub readconfig ($);
44b7fe58 181sub readconfig ($) {
8ca56de8 182 local ($cfgpath) = @_;
4e5552a4
IJ
183
184 my $dirfh;
185 if (opendir $dirfh, $cfgpath) {
186 while ($!=0, my $ent = readdir $dirfh) {
187 next if $ent =~ m/[^-A-Za-z0-9_]/;
188 readconfig "$cfgpath/$ent";
189 }
190 die "$0: $cfgpath: $!\n" if $!;
191 return;
192 }
193 die "$0: $cfgpath: $!\n" unless $!==ENOENT || $!==ENOTDIR;
194
8ca56de8 195 my $cfgfh = new IO::File $cfgpath, "<";
44b7fe58
IJ
196 if (!$cfgfh) {
197 die "$0: $cfgpath: $!\n" unless $!==ENOENT;
198 return;
199 }
200 while (<$cfgfh>) {
201 s/^\s+//;
202 s/\s+$/\n/;
203 next if m/^\#/;
204 next unless m/\S/;
205 if (s{^permit\s+}{}) {
206 badcfg "v0config before permit" if defined $v0config;
8ca56de8
IJ
207 %need_allow = ();
208 need_allow_singleton 'Caller', allowent 'caller';
209 need_allow_singleton 'Local',
210 allowent_addr "local interface", $local_addr;
211 need_allow_singleton 'Ifname', allowent 'interface name';
212 need_allow_item 'Remote',
213 allowent_addr "peer point-to-point addr", $peer_addr;
44b7fe58 214 foreach (@rnets) {
8ca56de8
IJ
215 need_allow_item 'Remote',
216 allowent_addr "remote network", $_;
44b7fe58 217 }
8ca56de8 218 #use Data::Dumper; print Dumper(\%need_allow);
44b7fe58 219 while (m{\S}) {
8ca56de8 220 if (s{^user\s+(\S+)\s+}{}) {
44b7fe58 221 maybe_allow_caller_env $1, 'USERV_USER', 'USERV_UID';
8ca56de8 222 } elsif (s{^group\s+(\S+)\s+}{}) {
44b7fe58
IJ
223 maybe_allow_caller_env $1, 'USERV_GROUP', 'USERV_GID';
224 } elsif (s{^everyone\s+}{}) {
225 maybe_allow_singleton 'Caller', 1;
226 } elsif (s{^hostnet\s+(\S+/\d+)\s+}{}) {
8ca56de8 227 my $hn = new NetAddr::IP::Lite $1 or
44b7fe58
IJ
228 badcfg "invalid ip address in hostnet";
229 my $host = new NetAddr::IP::Lite $hn->addr or die;
230 my $net = $hn->network() or die;
231 maybe_allow_addrs 'Local', $host;
8ca56de8 232 maybe_allow_addrs 'Remote', $net;
44b7fe58
IJ
233 } elsif (s{^(local|remote|addrs)\s+(\S+)\ s+}{}) {
234 my $h = $1;
235 my $s = new NetAddr::IP::Lite $2 or
236 badcfg "invalid ip address or mask in $h";
237 maybe_allow_addrs 'Local', $s if $h =~ m/addrs|local/;
238 maybe_allow_addrs 'Remote', $s if $h =~ m/addrs|remote/;
239 } elsif (s{^ifname\s+(\S+)\s+}{}) {
240 my ($spec) = $1;
241 maybe_allow_singleton 'Ifname', $ifname eq $spec;
242 } elsif (m{^\S+}) {
243 badcfg "unknown keyword in permit \`$1'";
244 } else {
245 die;
246 }
247 }
248 default_allow_singleton 'Ifname', $ifname eq $default_ifname;
249 my @wrong;
250 foreach my $clval (values %need_allow) {
251 foreach my $ne (@$clval) {
8ca56de8 252 next if $ne->{Allowed};
44b7fe58
IJ
253 push @wrong, $ne->{Desc};
254 }
255 }
256 if (!@wrong) {
257 # yay!
258 if ($protocol eq 'debug') {
8ca56de8 259 print "config $cfgpath:$.: matches\n";
44b7fe58
IJ
260 exit 0;
261 }
8ca56de8 262 execreal '*';
44b7fe58
IJ
263 }
264 if ($protocol eq 'debug') {
8ca56de8
IJ
265 #use Data::Dumper; print Dumper(\%need_allow);
266 print "config $cfgpath:$.: mismatch: $_\n"
44b7fe58
IJ
267 foreach @wrong;
268 }
269 } elsif (m{^v0config\s+(\S+)$}) {
270 badcfg "repeated v0config" if defined $v0config;
271 $v0config = $1;
272 } elsif (m{^include\s+(\S+)$}) {
273 readconfig $1;
274 } else {
275 badcfg "unknown config directive or bad syntax";
276 }
277 }
278 $cfgfh->error and die $!;
279 close $cfgfh;
8ca56de8
IJ
280
281 if (defined $v0config) {
282 $v0config =~ s{^}{./} unless $v0config =~ m{^/};
283 print "trying v0 config $v0config...\n" if $protocol eq 'debug';
284 execreal $v0config;
285 }
286 die "permission denied\n";
44b7fe58
IJ
287}
288
8ca56de8 289readconfig $v1config;