From: bodea Date: Wed, 17 Nov 2004 15:08:19 +0000 (+0000) Subject: - Add startup-config(5) manpage. X-Git-Tag: release_2_0_8~13 X-Git-Url: http://git.sameswireless.fr/l2tpns.git/commitdiff_plain/f5071c422df06800e59bb1be0218c0f6ec2ba831?hp=bf0a00f106b55399a0e335e7626a8c41c416ae0b - Add startup-config(5) manpage. - Add snoopctl, throttlectl plugins. - Update documentation. --- diff --git a/Changes b/Changes index dcc3e40..e761ef7 100644 --- a/Changes +++ b/Changes @@ -5,8 +5,9 @@ - Cleanup: make a bunch of global functions/variables static. - Remove reference to old -a command line argument. - Add l2tpns(8) and nsctl(8) manpages from Jonathan McDowell. -- Add startup-config(5) manpage [FIXME]. +- Add startup-config(5) manpage. - Revise nsctl to allow arbitrary strings/args to be passed to plugins. +- Add snoopctl, throttlectl plugins. * Mon Nov 15 2004 Brendan O'Dea 2.0.7 - Fix socket creation in host_unreachable() (thanks to Bjørn Augestad) diff --git a/Docs/manual.html b/Docs/manual.html index ba62c5d..59d6dbc 100644 --- a/Docs/manual.html +++ b/Docs/manual.html @@ -146,6 +146,7 @@ set ipaddress 192.168.1.1 set boolean true +

-
  • as_number (int)
    -Defines the local AS number for BGP (see Routing). -

    -

  • +

    BGP routing configuration is entered by the command: +The routing configuration section is entered by the command +

    router bgp as
    +where as specifies the local AS number. -
  • bgp_peer1 (string) -
  • bgp_peer1_as (int) -
  • bgp_peer2 (string) -
  • bgp_peer2_as (int)
    -

    -DNS name (or IP) and AS number of BGP peers. -

  • - +

    Subsequent lines prefixed with +

    neighbour peer
    +define the attributes of BGP neighhbours. Valid commands are: +
    +
    neighbour peer remote-as as +
    neighbout peer timers keepalive hold +
    + +Where peer specifies the BGP neighbour as either a hostname or +IP address, as is the remote AS number and keepalive, +hold are the timer values in seconds.

    users

    @@ -411,8 +395,7 @@ A running l2tpns process can be controlled in a number of ways. The primary method of control is by the Command-Line Interface (CLI).

    You can also remotely send commands to modules via the nsctl client -provided. This currently only works with the walled garden module, but -modification is trivial to support other modules.

    +provided.

    Also, there are a number of signals that l2tpns understands and takes action when it receives them. @@ -642,16 +625,13 @@ this way, although some may require a restart to take effect.

    nsctl

    -nsctl was implemented (badly) to allow messages to be passed to modules.

    +nsctl allows messages to be passed to plugins.

    -You must pass at least 2 parameters: host and command. The -host is the address of the l2tpns server which you want to send the message -to.

    +Arguments are command and optional args. See +nsctl(8) for more details.

    -Command can currently be either garden or ungarden. With -both of these commands, you must give a session ID as the 3rd parameter. -This will activate or deactivate the walled garden for a session -temporarily. +Built-in command are load_plugin, unload_plugin and +help. Any other commands are passed to plugins for processing.

    Signals

    diff --git a/Docs/nsctl.8 b/Docs/nsctl.8 index 6c6bd22..b7613f6 100644 --- a/Docs/nsctl.8 +++ b/Docs/nsctl.8 @@ -2,10 +2,10 @@ .de Id .ds Dt \\$4 \\$5 .. -.Id $Id: nsctl.8,v 1.1 2004/11/17 08:23:35 bodea Exp $ +.Id $Id: nsctl.8,v 1.2 2004/11/17 15:08:19 bodea Exp $ .TH NSCTL 8 "\*(Dt" L2TPNS "System Management Commands" .SH NAME -nsctl \- Issue commands to l2tpns plugins +nsctl \- manage running l2tpns instance .SH SYNOPSIS .B nsctl .RB [ \-d ] @@ -17,10 +17,10 @@ nsctl \- Issue commands to l2tpns plugins .RI [ arg " ...]" .SH DESCRIPTION .B nsctl -is part of the +sends commands to a running .B l2tpns -package. It allows the system administrator to send manage plugin -features of a running l2tpns process. +process. It provides both for the loading or unloading of plugins and +also the management of sessions via functions provided by those plugins. .SH OPTIONS .TP .B \-d @@ -59,7 +59,9 @@ Any other value of if any) are sent to .B l2tpns -as-is, to be passed to each plugin in turn (and possibly acted upon). +as-is, to be passed to each plugin which registers a +.B plugin_control +function in turn (in which it may be acted upon). .SH SEE ALSO .BR l2tpns (8) .SH AUTHOR diff --git a/Docs/startup-config.5 b/Docs/startup-config.5 new file mode 100644 index 0000000..35527f3 --- /dev/null +++ b/Docs/startup-config.5 @@ -0,0 +1,203 @@ +.\" -*- nroff -*- +.de Id +.ds Dt \\$4 \\$5 +.. +.Id $Id: startup-config.5,v 1.1 2004/11/17 15:08:19 bodea Exp $ +.TH STARTUP-CONFIG 5 "\*(Dt" L2TPNS "File Formats and Conventions" +.SH NAME +startup\-config \- configuration file for l2tpns +.SH SYNOPSIS +/etc/l2tpns/startup-config +.SH DESCRIPTION +.B startup-config +is the configuration file for +.BR l2tpns . +.PP +The format is plain text, in the same format as accepted by the +configuration mode of +.BR l2tpns 's +telnet administrative interface. Comments are indicated by either the +character +.B # +or +.BR ! . +.SS SETTINGS +Settings are specified with +.IP +.BI "set " "variable value" +.PP +The following +.IR variable s +may be set: +.RS +.TP +.B debug +Set the level of debugging messages written to the log file. The +value should be between 0 and 5, with 0 being no debugging, and 5 +being the highest. +.TP +.B log_file +This will be where all logging and debugging information is written +to. This may be either a filename, such as +.BR /var/log/l2tpns , +or the string +.BR syslog : \fIfacility\fR , +where +.I facility +is any one of the syslog logging facilities, such as +.BR local5 . +.TP +.B pid_file +If set, the process id will be written to the specified file. The +value must be an absolute path. +.TP +.B l2tp_secret +The secret used by +.B l2tpns +for authenticating tunnel request. Must be the same as the LAC, or +authentication will fail. Only actually be used if the LAC requests +authentication. +.TP +.BR primary_dns , " secondary_dns" +Whenever a PPP connection is established, DNS servers will be sent to the +user, both a primary and a secondary. If either is set to 0.0.0.0, then that +one will not be sent. +.TP +.B save_state +When +.B l2tpns +receives a STGTERM it will write out its current ip_address_pool, +session and tunnel tables to disk prior to exiting to be re-loaded at +startup. The validity of this data is obviously quite short and the +intent is to allow an sessions to be retained over a software upgrade. +.TP +.BR primary_radius , " secondary_radius" +Sets the RADIUS servers used for both authentication and accounting. +If the primary server does not respond, then the secondary RADIUS +server will be tried. +.TP +.BR primary_radius_port , " secondary_radius_port" +Sets the authentication ports for the primary and secondary RADIUS +servers. The accounting port is one more than the authentication +port. If no ports are given, authentication defaults to 1645, and +accounting to 1646. +.TP +.B radius_accounting +If set to true, then RADIUS accounting packets will be sent. A +.B Start +record will be sent when the session is successfully authenticated, +and a +.B Stop +record when the session is closed. +.TP +.B radius_secret +Secret to be used in RADIUS packets. +.TP +.B bind_address +When the tun interface is created, it is assigned the address +specified here. If no address is given, 1.1.1.1 is used. Packets +containing user traffic should be routed via this address if given, +otherwise the primary address of the machine. +.TP +.B peer_address +Address to send to clients as the default gateway. +.TP +.B send_garp +Determines whether or not to send a gratuitous ARP for the +.B bind_address +when the server is ready to handle traffic (default: true). This +setting is ignored if BGP is configured. +.TP +.B throttle_speed +Sets the default speed (in kbits/s) which sessions will be limited to. +.TP +.B throttle_buckets +Number of token buckets to allocate for throttling. Each throttled +session requires two buckets (in and out). +.TP +.B accounting_dir +If set to a directory, then every 5 minutes the current usage for +every connected use will be dumped to a file in this directory. +.TP +.B setuid +After starting up and binding the interface, change UID to this. This +doesn't work properly. +.TP +.B dump_speed +If set to true, then the current bandwidth utilization will be logged +every second. Even if this is disabled, you can see this information +by running the +.B +uptime +command on the CLI. +.TP +.B cleanup_interval +Interval between regular cleanups (in seconds). +.TP +.B multi_read_count +Number of packets to read off each of the UDP and TUN fds when +returned as readable by select (default: 10). Avoids incurring the +unnecessary system call overhead of select on busy servers. +.TP +.B scheduler_fifo +Sets the scheduling policy for the +.B l2tpns +process to +.BR SCHED_FIFO . +This causes the kernel to immediately preempt any currently running +.B SCHED_OTHER +(normal) process in favour of +.B l2tpns +when it becomes runnable. +.br +Ignored on uniprocessor systems. +.TP +.B lock_pages +Keep all pages mapped by the +.B l2tpns +process in memory. +.TP +.B icmp_rate +Maximum number of host unreachable ICMP packets to send per second. +.TP +.B cluster_address +Multicast cluster address (default: 239.192.13.13). +.TP +.B cluster_interface +Interface for cluster packets (default: eth0). +.TP +.B cluster_hb_interval +Interval in tenths of a second between cluster heartbeat/pings. +.TP +.B cluster_hb_timeout +Cluster heartbeat timeout in tenths of a second. A new master will be +elected when this interval has been passed without seeing a heartbeat +from the master. +.RE +.SS BGP ROUTING +The routing configuration section is entered by the command +.IP +.BI "router bgp " as +.PP +where +.I as +specifies the local AS number. +.PP +Subsequent lines prefixed with +.BI "neighbour " peer +define the attributes of BGP neighhbours. Valid commands are: +.IP +.BI "neighbour " peer " remote-as " as +.br +.BI "neighbour " peer " timers " "keepalive hold" +.PP +Where +.I peer +specifies the BGP neighbour as either a hostname or IP address, +.I as +is the remote AS number and +.IR keepalive , +.I hold +are the timer values in seconds. +.SH SEE ALSO +.BR l2tpns (8) diff --git a/Makefile b/Makefile index 3c203c1..5d61124 100644 --- a/Makefile +++ b/Makefile @@ -31,22 +31,12 @@ LDFLAGS = LDLIBS = -lm INSTALL = install -c -D -o root -g root -OBJS = arp.o \ - bgp.o \ - cli.o \ - cluster.o \ - constants.o \ - control.o \ - icmp.o \ - l2tpns.o \ - ll.o \ - md5.o \ - ppp.o \ - radius.o \ - tbf.o \ - util.o - -PLUGINS = garden.so autothrottle.so autosnoop.so stripdomain.so setrxspeed.so +OBJS = arp.o bgp.o cli.o cluster.o constants.o control.o icmp.o \ + l2tpns.o ll.o md5.o ppp.o radius.o tbf.o util.o + +PLUGINS = garden.so throttlectl.so autothrottle.so snoopctl.so \ + autosnoop.so stripdomain.so setrxspeed.so + TARGETS = l2tpns nsctl generateload bounce $(PLUGINS) all: $(TARGETS) @@ -100,6 +90,7 @@ install: all echo '***' Installing default config files in $(DESTDIR)$(etcdir) - remember to adjust them; \ suffix=; \ fi; \ + $(INSTALL) -m 0600 etc/startup-config.default $(DESTDIR)$(etcdir)/startup-config$$suffix; \ $(INSTALL) -m 0644 etc/ip_pool.default $(DESTDIR)$(etcdir)/ip_pool$$suffix; \ $(INSTALL) -m 0600 etc/users.default $(DESTDIR)$(etcdir)/users$$suffix @@ -132,7 +123,9 @@ radius.o: radius.c md5.h constants.h l2tpns.h plugin.h util.h tbf.o: tbf.c l2tpns.h util.h tbf.h util.o: util.c l2tpns.h bgp.h garden.so: garden.c l2tpns.h plugin.h control.h +throttlectl.so: throttlectl.c l2tpns.h plugin.h control.h autothrottle.so: autothrottle.c l2tpns.h plugin.h +snoopctl.so: snoopctl.c l2tpns.h plugin.h control.h autosnoop.so: autosnoop.c l2tpns.h plugin.h stripdomain.so: stripdomain.c l2tpns.h plugin.h setrxspeed.so: setrxspeed.c l2tpns.h plugin.h diff --git a/etc/startup-config.default b/etc/startup-config.default index b2c814c..0936b6b 100644 --- a/etc/startup-config.default +++ b/etc/startup-config.default @@ -17,5 +17,7 @@ set accounting_dir "/var/run/l2tpns/acct/" set setuid 0 set dump_speed no load plugin "garden" +load plugin "throttlectl" load plugin "autothrottle" +load plugin "snoopctl" load plugin "autosnoop" diff --git a/garden.c b/garden.c index 2396ffd..125cc44 100644 --- a/garden.c +++ b/garden.c @@ -9,7 +9,7 @@ /* walled garden */ -char const *cvs_id = "$Id: garden.c,v 1.13 2004/11/17 08:23:34 bodea Exp $"; +char const *cvs_id = "$Id: garden.c,v 1.14 2004/11/17 15:08:19 bodea Exp $"; int plugin_api_version = PLUGIN_API_VERSION; static struct pluginfuncs *p = 0; @@ -76,22 +76,25 @@ int plugin_kill_session(struct param_new_session *data) } char *plugin_control_help[] = { - " garden USER|SID Put user into the walled garden", - " ungarden USER|SID Release user", + " garden USER|SID Put user into the walled garden", + " ungarden USER|SID Release user from garden", 0 }; int plugin_control(struct param_control *data) { - sessionidt session; - sessiont *s = 0; + sessionidt s; + sessiont *sess = 0; int flag; char *end; - if (data->argc < 1 || (strcmp(data->argv[0], "garden") && strcmp(data->argv[0], "ungarden"))) + if (data->argc < 1) + return PLUGIN_RET_OK; + + if (strcmp(data->argv[0], "garden") && strcmp(data->argv[0], "ungarden")) return PLUGIN_RET_OK; // not for us - flag = data->argv[0][0] == 'g'; + flag = data->argv[0][0] != 'u'; if (!iam_master) // All garden processing happens on the master. { @@ -107,8 +110,8 @@ int plugin_control(struct param_control *data) return PLUGIN_RET_STOP; } - if (!(session = strtol(data->argv[0], &end, 10)) || *end) - session = p->get_session_by_username(data->argv[0]); + if (!(session = strtol(data->argv[1], &end, 10)) || *end) + session = p->get_session_by_username(data->argv[1]); if (session) s = p->get_session_by_id(session); @@ -128,6 +131,8 @@ int plugin_control(struct param_control *data) } garden_session(s, flag); + p->sesssion_changed(session); + data->response = NSCTL_RES_OK; data->additional = 0; @@ -149,7 +154,7 @@ int plugin_become_master(void) } // Called for each active session after becoming master -int plugin_new_session_master(sessiont * s) +int plugin_new_session_master(sessiont *s) { if (s->walled_garden) { diff --git a/l2tpns.c b/l2tpns.c index 4bdf862..8c23a9a 100644 --- a/l2tpns.c +++ b/l2tpns.c @@ -4,7 +4,7 @@ // Copyright (c) 2002 FireBrick (Andrews & Arnold Ltd / Watchfront Ltd) - GPL licenced // vim: sw=8 ts=8 -char const *cvs_id_l2tpns = "$Id: l2tpns.c,v 1.51 2004/11/17 08:23:34 bodea Exp $"; +char const *cvs_id_l2tpns = "$Id: l2tpns.c,v 1.52 2004/11/17 15:08:19 bodea Exp $"; #include #include @@ -3760,10 +3760,12 @@ static int add_plugin(char *plugin_name) sessionbyuser, sessiontbysessionidt, sessionidtbysessiont, - sessionkill, radiusnew, radiussend, getconfig, + sessionkill, + throttle_session, + cluster_send_session, }; void *p = open_plugin(plugin_name, 1); diff --git a/nsctl.c b/nsctl.c index 2ad6a7a..057edfa 100644 --- a/nsctl.c +++ b/nsctl.c @@ -16,13 +16,12 @@ struct { char *usage; int action; } builtins[] = { - { "load_plugin", " PLUGIN Load named plugin", NSCTL_REQ_LOAD }, - { "unload_plugin", " PLUGIN Unload named plugin", NSCTL_REQ_UNLOAD }, - { "help", " List available commands", NSCTL_REQ_HELP }, + { "load_plugin", " PLUGIN Load named plugin", NSCTL_REQ_LOAD }, + { "unload_plugin", " PLUGIN Unload named plugin", NSCTL_REQ_UNLOAD }, + { "help", " List available commands", NSCTL_REQ_HELP }, { 0 } }; - static int debug = 0; static int timeout = 2; // 2 seconds static char *me; diff --git a/plugin.h b/plugin.h index b751a1d..89ef8e0 100644 --- a/plugin.h +++ b/plugin.h @@ -31,10 +31,12 @@ struct pluginfuncs sessionidt (*get_session_by_username)(char *username); sessiont *(*get_session_by_id)(sessionidt s); sessionidt (*get_id_by_session)(sessiont *s); - void (*sessionkill)(sessionidt s, char *reason); u16 (*radiusnew)(sessionidt s); void (*radiussend)(u16 r, u8 state); void *(*getconfig)(char *key, enum config_typet type); + void (*sessionkill)(sessionidt s, char *reason); + void (*throttle)(sessionidt s, int rate_in, int rate_out); + int (*session_changed)(int sid); }; struct param_pre_auth diff --git a/snoopctl.c b/snoopctl.c new file mode 100644 index 0000000..8c7c3d5 --- /dev/null +++ b/snoopctl.c @@ -0,0 +1,138 @@ +#include +#include "l2tpns.h" +#include "plugin.h" +#include "control.h" + +/* snoop control */ + +char const *cvs_id = "$Id: snoopctl.c,v 1.1 2004/11/17 15:08:19 bodea Exp $"; + +int plugin_api_version = PLUGIN_API_VERSION; +static struct pluginfuncs *p = 0; + +char *plugin_control_help[] = { + " snoop USER|SID IP PORT Intercept user traffic", + " unsnoop USER|SID Stop intercepting user", + 0 +}; + +static int iam_master = 0; + +int plugin_init(struct pluginfuncs *funcs) +{ + if (!funcs) + return 0; + + p = funcs; + return 1; +} + +int plugin_become_master(void) +{ + iam_master = 1; + return PLUGIN_RET_OK; +} + +int plugin_control(struct param_control *data) +{ + sessionidt session; + sessiont *s = 0; + int flag; + char *end; + + if (data->argc < 1) + return PLUGIN_RET_OK; + + if (strcmp(data->argv[0], "snoop") && strcmp(data->argv[0], "unsnoop")) + return PLUGIN_RET_OK; // not for us + + flag = data->argv[0][0] != 'u'; + + if (!iam_master) + { + data->response = NSCTL_RES_ERR; + data->additional = "must be run on the cluster master"; + return PLUGIN_RET_STOP; + } + + if (flag) + { + if (data->argc != 4) + { + data->response = NSCTL_RES_ERR; + data->additional = "requires username or session id and host, port"; + return PLUGIN_RET_STOP; + } + } + else + { + if (data->argc != 2) + { + data->response = NSCTL_RES_ERR; + data->additional = "requires username or session id"; + return PLUGIN_RET_STOP; + } + } + + if (!(session = strtol(data->argv[1], &end, 10)) || *end) + session = p->get_session_by_username(data->argv[1]); + + if (session) + s = p->get_session_by_id(session); + + if (!s || !s->ip) + { + data->response = NSCTL_RES_ERR; + data->additional = "session not found"; + return PLUGIN_RET_STOP; + } + + if (flag) + { + ipt ip = inet_addr(data->argv[2]); + u16 port = atoi(data->argv[3]); + + if (!ip || ip == INADDR_NONE) + { + data->response = NSCTL_RES_ERR; + data->additional = "invalid ip address"; + return PLUGIN_RET_STOP; + } + + if (!port) + { + data->response = NSCTL_RES_ERR; + data->additional = "invalid port"; + return PLUGIN_RET_STOP; + } + + if (ip == s->snoop_ip && port == s->snoop_port) + { + data->response = NSCTL_RES_ERR; + data->additional = "already intercepted"; + return PLUGIN_RET_STOP; + } + + s->snoop_ip = ip; + s->snoop_port = port; + } + else + { + if (!s->snoop_ip) + { + data->response = NSCTL_RES_ERR; + data->additional = "not intercepted"; + return PLUGIN_RET_STOP; + } + + s->snoop_ip = 0; + s->snoop_port = 0; + } + + p->sesssion_changed(session); + + data->response = NSCTL_RES_OK; + data->additional = 0; + + return PLUGIN_RET_STOP; +} diff --git a/throttlectl.c b/throttlectl.c new file mode 100644 index 0000000..565c4d8 --- /dev/null +++ b/throttlectl.c @@ -0,0 +1,151 @@ +#include +#include "l2tpns.h" +#include "plugin.h" +#include "control.h" + +/* throttle control */ + +char const *cvs_id = "$Id: throttlectl.c,v 1.1 2004/11/17 15:08:19 bodea Exp $"; + +int plugin_api_version = PLUGIN_API_VERSION; +static struct pluginfuncs *p = 0; + +char *plugin_control_help[] = { + " throttle USER|SID [RATE|[in|out] RATE ...] Throttle user traffic", + " unthrottle USER|SID Stop throttling user", + 0 +}; + +static int iam_master = 0; + +int plugin_init(struct pluginfuncs *funcs) +{ + if (!funcs) + return 0; + + p = funcs; + return 1; +} + +int plugin_become_master(void) +{ + iam_master = 1; + return PLUGIN_RET_OK; +} + +int plugin_control(struct param_control *data) +{ + sessionidt session; + sessiont *s = 0; + int flag; + char *end; + int rate_in = 0; + int rate_out = 0; + + if (data->argc < 1) + return PLUGIN_RET_OK; + + if (strcmp(data->argv[0], "throttle") + && strcmp(data->argv[0], "unthrottle")) + return PLUGIN_RET_OK; // not for us + + flag = data->argv[0][0] != 'g'; + + if (!iam_master) + { + data->response = NSCTL_RES_ERR; + data->additional = "must be run on the cluster master"; + return PLUGIN_RET_STOP; + } + + if (flag) + { + if (data->argc < 2 || data->argc > 4) + { + data->response = NSCTL_RES_ERR; + data->additional = "requires username or session id and optional rate(s)"; + return PLUGIN_RET_STOP; + } + } + else + { + if (data->argc != 2) + { + data->response = NSCTL_RES_ERR; + data->additional = "requires username or session id"; + return PLUGIN_RET_STOP; + } + } + + if (!(session = strtol(data->argv[1], &end, 10)) || *end) + session = p->get_session_by_username(data->argv[1]); + + if (session) + s = p->get_session_by_id(session); + + if (!s || !s->ip) + { + data->response = NSCTL_RES_ERR; + data->additional = "session not found"; + return PLUGIN_RET_STOP; + } + + if (flag) + { + rate_in = rate_out = -1; + if (data->argc == 2) + { + unsigned long *rate = p->getconfig("throttle_speed", UNSIGNED_LONG); + rate_in = rate_out = *rate; + } + else if (data->argc == 3) + { + rate_in = rate_out = atoi(data->argv[2]); + } + else + { + int i; + for (i = 2; i < data->argc - 1; i += 2) + { + int len = strlen(data->argv[i]); + if (!strncmp(data->argv[i], "in", len)) + { + rate_in = atoi(argv[i+1]); + } + else if (!strncmp(data->argv[i], "out", len)) + { + rate_out = atoi(argv[i+1]); + } + else + { + data->response = NSCTL_RES_ERR; + data->additional = "invalid rate"; + return PLUGIN_RET_STOP; + } + } + } + + if (!rate_in || !rate_out) + { + data->response = NSCTL_RES_ERR; + data->additional = "invalid rate"; + return PLUGIN_RET_STOP; + } + } + + if (rate_in != -1 && rate_in == s->throttle_in && + rate_out != -1 && rate_out == s->throttle_out) + { + data->response = NSCTL_RES_ERR; + data->additional = flag ? "already throttled" : "not throttled"; + return PLUGIN_RET_STOP; + } + + p->throttle(session, rate_in, rate_out); + p->sesssion_changed(session); + + data->response = NSCTL_RES_OK; + data->additional = 0; + + return PLUGIN_RET_STOP; +}